@muxoai/platform 1.4.0 → 1.4.2

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 (62) 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 +2 -2
  5. package/dist/cli.js +323 -13
  6. package/dist/composio-catalog.d.ts +234 -0
  7. package/dist/composio-catalog.js +1894 -0
  8. package/dist/config.d.ts +16 -0
  9. package/dist/config.js +27 -1
  10. package/dist/durable-workflow.d.ts +45 -0
  11. package/dist/durable-workflow.js +90 -0
  12. package/dist/engine.d.ts +293 -5
  13. package/dist/engine.js +1502 -88
  14. package/dist/mcp-schema.d.ts +286 -0
  15. package/dist/mcp-schema.js +2559 -0
  16. package/dist/mcp.d.ts +2 -30
  17. package/dist/mcp.js +913 -558
  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 +182 -1
  24. package/dist/policy.js +1524 -0
  25. package/dist/providers/composio-instant.js +262 -7
  26. package/dist/providers/composio-sdk.d.ts +125 -5
  27. package/dist/providers/composio-sdk.js +901 -82
  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 +455 -3
  33. package/dist/providers/stripe-projects.js +2570 -129
  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 +440 -3
  39. package/dist/sdk.js +305 -3
  40. package/dist/server.js +2343 -264
  41. package/dist/store.d.ts +113 -6
  42. package/dist/store.js +677 -52
  43. package/dist/types.d.ts +207 -3
  44. package/dist/ui/console-html.d.ts +40 -1
  45. package/dist/ui/console-html.js +1813 -629
  46. package/dist/ui/design-tokens.js +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +4 -3
  50. package/plugins/muxo/.claude-plugin/plugin.json +2 -2
  51. package/plugins/muxo/.codex-plugin/plugin.json +15 -8
  52. package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
  53. package/plugins/muxo/hooks/session-start.sh +15 -3
  54. package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
  55. package/plugins/muxo/mcp.json +0 -10
  56. package/plugins/muxo/plugin.json +4 -4
  57. package/plugins/muxo/skills/muxo/SKILL.md +59 -9
  58. package/plugins/muxo/skills/muxo/references/cli.md +5 -1
  59. package/plugins/muxo/skills/muxo/references/mcp.md +13 -7
  60. package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
  61. package/plugins/muxo/skills/setup/SKILL.md +9 -1
  62. package/src/catalogues/stripe-projects-catalog.json +140 -0
@@ -2,8 +2,149 @@ import { execFile } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
+ import { buildStandardStripeProjectsSecrets, extractDatabaseMigrationsFromInput, extractHostedHtmlFromInput, isStripeProjectsDatabaseProvider, isStripeProjectsExclusiveProvider, isStripeProjectsHostingProvider, normalizeStripeProjectsProviderSlug, renderDefaultHostedSiteHtml, resolveStripeProjectsCanonicalToolSlug, STRIPE_PROJECTS_DEPLOYABLE_CATALOG, } from "../composio-catalog.js";
5
6
  import { ROOT_DIR } from "../config.js";
6
7
  const execFileAsync = promisify(execFile);
8
+ export const DEFAULT_STRIPE_PROVISIONING_API_VERSION = "2026-09-30.preview";
9
+ const SENSITIVE_FIELD_PATTERN = /secret|token|password|credential|access_configuration|confirmation_secret|checkout_session_url|redirect_url|database_url|api_key|private_key/i;
10
+ export function redactProvisioningSecrets(value) {
11
+ if (!value || typeof value !== "object")
12
+ return null;
13
+ const out = {};
14
+ for (const [k, v] of Object.entries(value)) {
15
+ if (SENSITIVE_FIELD_PATTERN.test(k)) {
16
+ out[k] = "[REDACTED]";
17
+ }
18
+ else if (v && typeof v === "object" && !Array.isArray(v)) {
19
+ out[k] = redactProvisioningSecrets(v);
20
+ }
21
+ else {
22
+ out[k] = v;
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+ export function classifyStripeProvisioningError(err) {
28
+ const code = String(err.code || "").toLowerCase();
29
+ const msg = String(err.message || "");
30
+ if (code === "payment_method_required") {
31
+ return {
32
+ platformAction: "Complete payment setup via POST /v2/provisioning/payment_method_requests, obtain user approval, then retry deliberately.",
33
+ workflowStatus: "errored",
34
+ retryable: false,
35
+ };
36
+ }
37
+ if (code === "payment_method_and_customer_required") {
38
+ return {
39
+ platformAction: "For an enabled platform-source flow, provide matching source_customer and source_payment_method.",
40
+ workflowStatus: "errored",
41
+ retryable: false,
42
+ };
43
+ }
44
+ if (code === "payment_method_owner_required") {
45
+ return {
46
+ platformAction: "For an enabled platform-source flow, set payment_method_owner=platform.",
47
+ workflowStatus: "errored",
48
+ retryable: false,
49
+ };
50
+ }
51
+ if (code === "unsupported_payment_method_owner") {
52
+ return {
53
+ platformAction: "Stop; use only a currently enabled payment_method_owner mode.",
54
+ workflowStatus: "errored",
55
+ retryable: false,
56
+ };
57
+ }
58
+ if (code === "connect_relationship_required") {
59
+ return {
60
+ platformAction: "Verify the platform-to-connected account relationship and the Stripe-Context header value.",
61
+ workflowStatus: "errored",
62
+ retryable: false,
63
+ };
64
+ }
65
+ if (code === "provider_reauth_required") {
66
+ return {
67
+ platformAction: "Create a new Provider connection request (POST /v2/provisioning/provider_connection_requests) and ask the user to reconnect.",
68
+ workflowStatus: "errored",
69
+ retryable: false,
70
+ };
71
+ }
72
+ if (code === "invalid_resource_configuration") {
73
+ return {
74
+ platformAction: "Refresh the catalog service configuration_schema and collect corrected user input.",
75
+ workflowStatus: "errored",
76
+ retryable: false,
77
+ };
78
+ }
79
+ if (code === "resource_count_constraint_exceeded") {
80
+ return {
81
+ platformAction: "Explain the service constraint and offer an allowed update, association, or existing Resource.",
82
+ workflowStatus: "errored",
83
+ retryable: false,
84
+ };
85
+ }
86
+ if (code === "resource_not_complete") {
87
+ return {
88
+ 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.",
89
+ workflowStatus: "requires_review",
90
+ retryable: false,
91
+ };
92
+ }
93
+ if (code === "resource_access_configuration_unavailable") {
94
+ return {
95
+ platformAction: "Stop. Relink or rotate the Resource, or follow Provider guidance in status details.",
96
+ workflowStatus: "errored",
97
+ retryable: false,
98
+ };
99
+ }
100
+ if (code === "resource_access_configuration_expired") {
101
+ return {
102
+ platformAction: "Rotate the Resource credentials (POST /v2/provisioning/resources/{id}/rotate_credentials). If complete, retrieve replacement; if pending, set workflow status to requires_review.",
103
+ workflowStatus: "requires_review",
104
+ retryable: false,
105
+ };
106
+ }
107
+ if (err.status === 403) {
108
+ if (/provisioning_resource_reveal_access_configuration/i.test(msg)) {
109
+ return {
110
+ platformAction: "Use a restricted key with the provisioning_resource_reveal_access_configuration permission.",
111
+ workflowStatus: "errored",
112
+ retryable: false,
113
+ };
114
+ }
115
+ return {
116
+ platformAction: "Verify private-preview enrollment, the Stripe-Context connected account, and the Resource Project scope.",
117
+ workflowStatus: "errored",
118
+ retryable: false,
119
+ };
120
+ }
121
+ if (code === "rate_limited" || err.status === 429) {
122
+ return {
123
+ platformAction: "Back off with exponential delay and jitter, honoring Retry-After if present.",
124
+ workflowStatus: "requires_review",
125
+ retryable: true,
126
+ };
127
+ }
128
+ if (code === "provider_failure") {
129
+ return {
130
+ platformAction: "Show a safe message. Identify returned prerequisite when possible; retry only after approval and only if safe.",
131
+ workflowStatus: "requires_review",
132
+ retryable: false,
133
+ };
134
+ }
135
+ if (code === "not_found" || err.status === 404) {
136
+ return {
137
+ platformAction: "Verify object ID and Stripe-Context; do not probe another account.",
138
+ workflowStatus: "errored",
139
+ retryable: false,
140
+ };
141
+ }
142
+ return {
143
+ platformAction: "Stop, record safe diagnostics, retrieve known objects in the same Stripe-Context, mark requires_review, and do not replay the write automatically.",
144
+ workflowStatus: "requires_review",
145
+ retryable: false,
146
+ };
147
+ }
7
148
  export function isPaidCloudProvisioning(tool, input) {
8
149
  if (tool.lane !== "stripe_projects")
9
150
  return false;
@@ -24,18 +165,20 @@ export class StripeProjectsAdapter {
24
165
  config;
25
166
  tools = new Map();
26
167
  providerByName = new Map();
168
+ seededProviders = [];
169
+ seededServices = [];
27
170
  constructor(config) {
28
171
  this.config = config;
29
172
  this.loadCatalogSeed();
30
173
  }
31
174
  loadCatalogSeed() {
32
- // Lifecycle management tools for Stripe Projects
175
+ // Lifecycle management tools for Stripe Projects & Stripe v2 Provisioning API
33
176
  const lifecycleTools = [
34
177
  {
35
178
  slug: "resource.projects.status",
36
179
  upstreamSlug: "projects/status",
37
180
  name: "Cloud Infrastructure: Project & Provider Status",
38
- description: "Inspect active provisioned resources, linked cloud providers (Neon, Supabase, Turso, Cloudflare, Vercel, etc.), and environment status.",
181
+ description: "Inspect active provisioned resources, linked cloud providers (Neon, Supabase, Turso, Cloudflare, Vercel, etc.), and environment status via Stripe v2 Provisioning.",
39
182
  lane: "stripe_projects",
40
183
  providerSlug: "projects",
41
184
  providerName: "Muxo Cloud Infrastructure",
@@ -49,11 +192,169 @@ export class StripeProjectsAdapter {
49
192
  properties: {},
50
193
  },
51
194
  },
195
+ {
196
+ slug: "resource.projects.eligibility",
197
+ upstreamSlug: "v2/provisioning/eligibility",
198
+ name: "Stripe v2 Provisioning: Check Eligibility",
199
+ description: "Check whether the workspace or Stripe account is eligible for Stripe v2 Provisioning (GET /v2/provisioning/eligibility) and inspect any outstanding requirements.",
200
+ lane: "stripe_projects",
201
+ providerSlug: "projects",
202
+ providerName: "Muxo Cloud Infrastructure",
203
+ categories: ["infrastructure", "eligibility", "status"],
204
+ riskLevel: "read",
205
+ requiresAuth: false,
206
+ creditCost: 0,
207
+ billingMode: "resource_provisioning",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {},
211
+ },
212
+ },
213
+ {
214
+ slug: "resource.projects.catalog",
215
+ upstreamSlug: "v2/provisioning/catalog",
216
+ name: "Stripe v2 Provisioning: List Catalog Providers & Services",
217
+ 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.",
218
+ lane: "stripe_projects",
219
+ providerSlug: "projects",
220
+ providerName: "Muxo Cloud Infrastructure",
221
+ categories: ["infrastructure", "catalog", "discovery"],
222
+ riskLevel: "read",
223
+ requiresAuth: false,
224
+ creditCost: 0,
225
+ billingMode: "resource_provisioning",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ catalog: {
230
+ type: "string",
231
+ enum: ["dev", "prod", "testing"],
232
+ default: "prod",
233
+ description: "Catalog partition (dev, prod, or testing)",
234
+ },
235
+ provider_name: {
236
+ type: "string",
237
+ description: "Optional provider name filter for catalog services",
238
+ },
239
+ limit: {
240
+ type: "number",
241
+ default: 25,
242
+ description: "Maximum items to return",
243
+ },
244
+ },
245
+ },
246
+ },
247
+ {
248
+ slug: "resource.projects.payment_profile",
249
+ upstreamSlug: "v2/provisioning/payment_profile",
250
+ name: "Stripe v2 Provisioning: Manage Payment Profile & Limits",
251
+ 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).",
252
+ lane: "stripe_projects",
253
+ providerSlug: "projects",
254
+ providerName: "Muxo Cloud Infrastructure",
255
+ categories: ["infrastructure", "billing", "payment_profile"],
256
+ riskLevel: "financial",
257
+ requiresAuth: false,
258
+ creditCost: 0,
259
+ billingMode: "resource_provisioning",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {
263
+ action: {
264
+ type: "string",
265
+ enum: ["retrieve", "update_limit", "request_payment_method"],
266
+ default: "retrieve",
267
+ description: "Payment profile action to execute",
268
+ },
269
+ provider: {
270
+ type: "string",
271
+ description: "Optional provider slug when updating a provider-specific limit",
272
+ },
273
+ max_amount: {
274
+ type: "number",
275
+ description: "Usage limit amount in cents (e.g. 5000 for $50.00)",
276
+ },
277
+ currency: {
278
+ type: "string",
279
+ default: "usd",
280
+ description: "Three-letter ISO currency code",
281
+ },
282
+ recurring_interval: {
283
+ type: "string",
284
+ enum: ["day", "week", "month", "year"],
285
+ default: "month",
286
+ description: "Usage limit recurring interval",
287
+ },
288
+ },
289
+ },
290
+ },
291
+ {
292
+ slug: "resource.projects.create_project",
293
+ upstreamSlug: "v2/provisioning/projects",
294
+ name: "Stripe v2 Provisioning: Create Project",
295
+ description: "Create a new Stripe v2 Provisioning project (POST /v2/provisioning/projects) in the specified catalog partition.",
296
+ lane: "stripe_projects",
297
+ providerSlug: "projects",
298
+ providerName: "Muxo Cloud Infrastructure",
299
+ categories: ["infrastructure", "projects"],
300
+ riskLevel: "provisioning",
301
+ requiresAuth: false,
302
+ creditCost: 0,
303
+ billingMode: "resource_provisioning",
304
+ inputSchema: {
305
+ type: "object",
306
+ required: ["name"],
307
+ properties: {
308
+ name: {
309
+ type: "string",
310
+ description: "Human-readable name for the new project",
311
+ },
312
+ catalog: {
313
+ type: "string",
314
+ enum: ["dev", "prod", "testing"],
315
+ default: "prod",
316
+ description: "Catalog partition to create the project in",
317
+ },
318
+ project_profile: {
319
+ type: "string",
320
+ description: "Optional developer profile identifier (prjprof_...)",
321
+ },
322
+ },
323
+ },
324
+ },
325
+ {
326
+ slug: "resource.projects.reveal_access_configuration",
327
+ upstreamSlug: "v2/provisioning/resources/reveal_access_configuration",
328
+ name: "Stripe v2 Provisioning: Reveal Resource Access Configuration",
329
+ 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.",
330
+ lane: "stripe_projects",
331
+ providerSlug: "projects",
332
+ providerName: "Muxo Cloud Infrastructure",
333
+ categories: ["infrastructure", "security", "secrets"],
334
+ riskLevel: "admin",
335
+ requiresAuth: false,
336
+ creditCost: 0,
337
+ billingMode: "resource_provisioning",
338
+ inputSchema: {
339
+ type: "object",
340
+ required: ["resource_id"],
341
+ properties: {
342
+ resource_id: {
343
+ type: "string",
344
+ description: "Stripe v2 Provisioning resource ID (res_... or fres_...)",
345
+ },
346
+ resource_name: {
347
+ type: "string",
348
+ description: "Optional local resource name in the workspace vault",
349
+ },
350
+ },
351
+ },
352
+ },
52
353
  {
53
354
  slug: "resource.projects.rotate_credentials",
54
355
  upstreamSlug: "projects/rotate",
55
356
  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.",
357
+ 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
358
  lane: "stripe_projects",
58
359
  providerSlug: "projects",
59
360
  providerName: "Muxo Cloud Infrastructure",
@@ -70,6 +371,10 @@ export class StripeProjectsAdapter {
70
371
  type: "string",
71
372
  description: "Local name of the provisioned resource (e.g. 'primary-db')",
72
373
  },
374
+ resource_id: {
375
+ type: "string",
376
+ description: "Optional Stripe v2 Provisioning resource ID (res_...)",
377
+ },
73
378
  },
74
379
  },
75
380
  },
@@ -77,7 +382,7 @@ export class StripeProjectsAdapter {
77
382
  slug: "resource.projects.deprovision",
78
383
  upstreamSlug: "projects/remove",
79
384
  name: "Cloud Infrastructure: Deprovision Resource",
80
- description: "Deprovision and remove a cloud resource at the upstream provider and purge its credentials from the workspace.",
385
+ 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
386
  lane: "stripe_projects",
82
387
  providerSlug: "projects",
83
388
  providerName: "Muxo Cloud Infrastructure",
@@ -94,10 +399,14 @@ export class StripeProjectsAdapter {
94
399
  type: "string",
95
400
  description: "Local name of the provisioned resource to remove",
96
401
  },
402
+ resource_id: {
403
+ type: "string",
404
+ description: "Optional Stripe v2 Provisioning resource ID (res_...)",
405
+ },
97
406
  untrack_only: {
98
407
  type: "boolean",
99
408
  default: false,
100
- description: "If true, untrack locally without deleting at the provider",
409
+ description: "If true, unlink without deleting at the provider",
101
410
  },
102
411
  },
103
412
  },
@@ -111,8 +420,88 @@ export class StripeProjectsAdapter {
111
420
  return;
112
421
  const raw = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
113
422
  for (const p of raw.providers || []) {
423
+ const normSlug = normalizeStripeProjectsProviderSlug(p.slug || p.name);
114
424
  this.providerByName.set(p.name.toLowerCase(), p);
115
425
  this.providerByName.set(p.slug.toLowerCase(), p);
426
+ if (normSlug) {
427
+ this.providerByName.set(normSlug, p);
428
+ }
429
+ this.seededProviders.push({
430
+ id: `prov_${p.slug}`,
431
+ object: "v2.provisioning.provider",
432
+ name: p.name,
433
+ description: p.description || null,
434
+ categories: p.categories || [],
435
+ capabilities: {
436
+ custom_configuration: true,
437
+ link_existing_resources: true,
438
+ },
439
+ configuration_schema: {},
440
+ llm_context: p.url ? `${p.url.replace(/\/+$/, "")}/llms.txt` : null,
441
+ website_url: p.url || null,
442
+ tos_url: p.url ? `${p.url.replace(/\/+$/, "")}/terms` : null,
443
+ privacy_policy_url: p.url ? `${p.url.replace(/\/+$/, "")}/privacy` : null,
444
+ });
445
+ }
446
+ for (const svc of raw.services || []) {
447
+ const providerKey = svc.provider_name.toLowerCase();
448
+ const normProv = normalizeStripeProjectsProviderSlug(svc.provider_name);
449
+ const matchedProvider = this.providerByName.get(providerKey) ||
450
+ this.providerByName.get(normProv);
451
+ const providerSlug = (normProv ||
452
+ matchedProvider?.slug ||
453
+ svc.provider_name.toLowerCase().replace(/[^a-z0-9]+/g, "")).toLowerCase();
454
+ if (svc.provider && matchedProvider) {
455
+ this.providerByName.set(svc.provider.toLowerCase(), matchedProvider);
456
+ }
457
+ this.seededServices.push({
458
+ id: svc.id,
459
+ object: "v2.provisioning.provider_service_detail",
460
+ provider: svc.provider || `prov_${providerSlug}`,
461
+ provider_name: svc.provider_name,
462
+ service_id: svc.service_id,
463
+ description: svc.description || null,
464
+ categories: svc.categories || [],
465
+ kind: svc.kind,
466
+ availability: svc.availability,
467
+ scope: "project",
468
+ configuration_schema: svc.configuration_schema || null,
469
+ pricing: svc.pricing || null,
470
+ llm_context: raw.service_metadata?.[svc.id]?.llm_context ||
471
+ (matchedProvider?.url
472
+ ? `${matchedProvider.url.replace(/\/+$/, "")}/llms.txt`
473
+ : null),
474
+ });
475
+ }
476
+ // Also index provider IDs from local .projects/state.local.json if available
477
+ try {
478
+ const localStatePath = path.join(this.config.stripeProjectsWorkdir, ".projects", "state.local.json");
479
+ if (fs.existsSync(localStatePath)) {
480
+ const localState = JSON.parse(fs.readFileSync(localStatePath, "utf8"));
481
+ for (const [slug, conn] of Object.entries(localState.providerConnections || {})) {
482
+ const existing = this.providerByName.get(slug.toLowerCase()) ||
483
+ (conn.providerName
484
+ ? this.providerByName.get(conn.providerName.toLowerCase())
485
+ : undefined) || {
486
+ slug: slug.toLowerCase(),
487
+ name: conn.providerName || slug,
488
+ url: "",
489
+ description: "",
490
+ categories: ["infrastructure"],
491
+ native_reference: `${slug}/project`,
492
+ };
493
+ this.providerByName.set(slug.toLowerCase(), existing);
494
+ if (conn.providerId) {
495
+ this.providerByName.set(conn.providerId.toLowerCase(), existing);
496
+ }
497
+ if (conn.providerName) {
498
+ this.providerByName.set(conn.providerName.toLowerCase(), existing);
499
+ }
500
+ }
501
+ }
502
+ }
503
+ catch {
504
+ // Ignore optional local state parsing issues
116
505
  }
117
506
  // Index available plan services per provider so companion plans sort free-tier first
118
507
  const providerPlans = new Map();
@@ -120,8 +509,11 @@ export class StripeProjectsAdapter {
120
509
  if (svc.availability !== "available" || svc.kind !== "plan")
121
510
  continue;
122
511
  const providerKey = svc.provider_name.toLowerCase();
123
- const matchedProvider = this.providerByName.get(providerKey);
124
- const providerSlug = (matchedProvider?.slug ||
512
+ const normProv = normalizeStripeProjectsProviderSlug(svc.provider_name);
513
+ const matchedProvider = this.providerByName.get(providerKey) ||
514
+ this.providerByName.get(normProv);
515
+ const providerSlug = (normProv ||
516
+ matchedProvider?.slug ||
125
517
  svc.provider_name.toLowerCase().replace(/[^a-z0-9]+/g, "")).toLowerCase();
126
518
  if (!providerPlans.has(providerSlug)) {
127
519
  providerPlans.set(providerSlug, new Map());
@@ -138,8 +530,11 @@ export class StripeProjectsAdapter {
138
530
  if (svc.availability !== "available")
139
531
  continue;
140
532
  const providerKey = svc.provider_name.toLowerCase();
141
- const matchedProvider = this.providerByName.get(providerKey);
142
- const providerSlug = (matchedProvider?.slug ||
533
+ const normProv = normalizeStripeProjectsProviderSlug(svc.provider_name);
534
+ const matchedProvider = this.providerByName.get(providerKey) ||
535
+ this.providerByName.get(normProv);
536
+ const providerSlug = (normProv ||
537
+ matchedProvider?.slug ||
143
538
  svc.provider_name.toLowerCase().replace(/[^a-z0-9]+/g, "")).toLowerCase();
144
539
  const serviceSlug = svc.service_id.toLowerCase();
145
540
  const muxoSlug = `resource.${providerSlug}.${serviceSlug.replace(/[^a-z0-9_]+/g, "_")}`;
@@ -175,16 +570,35 @@ export class StripeProjectsAdapter {
175
570
  ((pricingType.length > 0 && pricingType !== "free") ||
176
571
  /^(pro|scaler|paid|scale|business|team|grow|credits|launch|enterprise|plus)$/i.test(svc.service_id))) ||
177
572
  /^(credits|workers_paid)$/i.test(svc.service_id);
573
+ const isHosting = isStripeProjectsHostingProvider(providerSlug);
574
+ const isDatabase = isStripeProjectsDatabaseProvider(providerSlug);
178
575
  const tool = {
179
576
  slug: muxoSlug,
180
577
  upstreamSlug,
181
- name: `${svc.provider_name}: Provision ${svc.service_id} (${svc.kind})`,
182
- description: svc.description ||
183
- `Provision ${svc.provider_name} ${svc.service_id} via Muxo Cloud Infrastructure.`,
578
+ name: isHosting
579
+ ? `${svc.provider_name}: Provision & Deploy ${svc.service_id} (${svc.kind})`
580
+ : isDatabase
581
+ ? `${svc.provider_name}: Provision & Migrate ${svc.service_id} (${svc.kind})`
582
+ : `${svc.provider_name}: Provision ${svc.service_id} (${svc.kind})`,
583
+ description: isHosting
584
+ ? `${svc.description || `Provision ${svc.provider_name} ${svc.service_id} via Muxo Cloud Infrastructure.`} Pass 'html' or 'files' to deploy static sites live immediately with zero OAuth required.`
585
+ : isDatabase
586
+ ? `${svc.description || `Provision ${svc.provider_name} ${svc.service_id} via Muxo Cloud Infrastructure.`} Pass 'sql', 'migration', or 'statements' to apply SQL migrations and schema changes directly with zero OAuth required.`
587
+ : svc.description ||
588
+ `Provision ${svc.provider_name} ${svc.service_id} via Muxo Cloud Infrastructure.`,
184
589
  lane: "stripe_projects",
185
590
  providerSlug,
186
591
  providerName: svc.provider_name,
187
- categories: Array.from(new Set([...(svc.categories || []), "infrastructure"])),
592
+ categories: Array.from(new Set([
593
+ ...(svc.categories || []),
594
+ "infrastructure",
595
+ ...(isHosting
596
+ ? ["hosting", "deployment", "frontend", "static_site", "publish", "website"]
597
+ : []),
598
+ ...(isDatabase
599
+ ? ["database", "sql", "migration", "schema", "postgres", "storage"]
600
+ : []),
601
+ ])),
188
602
  riskLevel: "provisioning",
189
603
  requiresAuth: false,
190
604
  creditCost: isPaidService ? 25 : 5,
@@ -216,6 +630,39 @@ export class StripeProjectsAdapter {
216
630
  default: false,
217
631
  description: "Adopt an existing resource at the provider if supported",
218
632
  },
633
+ ...(isHosting
634
+ ? {
635
+ html: {
636
+ type: "string",
637
+ description: "Optional full HTML document (index.html) to publish and deploy immediately via Stripe Projects without OAuth handoff",
638
+ },
639
+ files: {
640
+ type: "object",
641
+ description: "Optional map of file paths to contents (e.g. {'index.html': '<!doctype html>...'}) to publish and deploy immediately",
642
+ },
643
+ env: {
644
+ type: "object",
645
+ description: "Optional map of environment variables (e.g. SUPABASE_URL, SUPABASE_ANON_KEY) to attach to the deployment",
646
+ },
647
+ }
648
+ : {}),
649
+ ...(isDatabase
650
+ ? {
651
+ sql: {
652
+ type: "string",
653
+ description: "Optional SQL DDL/migration script to execute immediately on the database via Stripe Projects (zero OAuth required)",
654
+ },
655
+ migration: {
656
+ type: "string",
657
+ description: "Optional SQL migration statement(s) to apply immediately without OAuth",
658
+ },
659
+ statements: {
660
+ type: "array",
661
+ items: { type: "string" },
662
+ description: "Optional array of SQL statements to execute in order on the provisioned database",
663
+ },
664
+ }
665
+ : {}),
219
666
  },
220
667
  },
221
668
  llmContextUrl: raw.service_metadata?.[svc.id]?.llm_context ||
@@ -240,6 +687,129 @@ export class StripeProjectsAdapter {
240
687
  };
241
688
  this.tools.set(muxoSlug, tool);
242
689
  }
690
+ // Ensure every deployable service in STRIPE_PROJECTS_DEPLOYABLE_CATALOG (94 entries across all 73 providers)
691
+ // and every canonical resource.<provider>.<service> slug is registered in this.tools for 100% parity
692
+ // between Node and the Cloudflare Worker EDGE_TOOLS catalog.
693
+ for (const entry of STRIPE_PROJECTS_DEPLOYABLE_CATALOG) {
694
+ if (this.tools.has(entry.slug)) {
695
+ const existing = this.tools.get(entry.slug);
696
+ if (!existing.llmContextUrl && entry.llmContextUrl) {
697
+ existing.llmContextUrl = entry.llmContextUrl;
698
+ }
699
+ if (!existing.providerUrl && entry.providerUrl) {
700
+ existing.providerUrl = entry.providerUrl;
701
+ }
702
+ if (existing.metadata &&
703
+ (!Array.isArray(existing.metadata.parentPlans) ||
704
+ existing.metadata.parentPlans.length === 0) &&
705
+ entry.availablePlans.length > 0) {
706
+ existing.metadata.parentPlans = entry.availablePlans;
707
+ }
708
+ continue;
709
+ }
710
+ const providerSlug = entry.providerSlug;
711
+ const existingForProvider = Array.from(this.tools.values()).find((t) => t.providerSlug === providerSlug && t.metadata?.kind === "deployable") || Array.from(this.tools.values()).find((t) => t.providerSlug === providerSlug);
712
+ const isHosting = isStripeProjectsHostingProvider(providerSlug);
713
+ const isDatabase = isStripeProjectsDatabaseProvider(providerSlug);
714
+ const parentPlans = (Array.isArray(existingForProvider?.metadata?.parentPlans) &&
715
+ existingForProvider.metadata.parentPlans.length > 0
716
+ ? existingForProvider.metadata.parentPlans
717
+ : entry.availablePlans) || [entry.defaultPlan];
718
+ this.tools.set(entry.slug, {
719
+ slug: entry.slug,
720
+ upstreamSlug: entry.upstreamSlug,
721
+ name: entry.name,
722
+ description: entry.description,
723
+ lane: "stripe_projects",
724
+ providerSlug,
725
+ providerName: entry.providerName,
726
+ categories: Array.from(new Set([
727
+ ...entry.categories,
728
+ ...(existingForProvider?.categories || []),
729
+ "infrastructure",
730
+ ...(isHosting
731
+ ? ["hosting", "deployment", "frontend", "static_site", "publish", "website"]
732
+ : []),
733
+ ...(isDatabase
734
+ ? ["database", "sql", "migration", "schema", "postgres", "storage"]
735
+ : []),
736
+ ])),
737
+ riskLevel: "provisioning",
738
+ requiresAuth: false,
739
+ creditCost: 5,
740
+ billingMode: "resource_provisioning",
741
+ inputSchema: {
742
+ type: "object",
743
+ properties: {
744
+ resource_name: {
745
+ type: "string",
746
+ description: `Project or resource name (defaults to ${providerSlug}-${entry.serviceId})`,
747
+ },
748
+ plan: {
749
+ type: "string",
750
+ default: parentPlans[0] || entry.defaultPlan,
751
+ description: parentPlans.length > 0
752
+ ? `Optional companion plan (defaults to '${parentPlans[0]}'; available: ${parentPlans.join(", ")})`
753
+ : "Plan tier",
754
+ },
755
+ config: existingForProvider?.inputSchema?.properties?.config || {
756
+ type: "object",
757
+ description: "Optional provider-specific service configuration JSON",
758
+ },
759
+ existing: {
760
+ type: "boolean",
761
+ default: false,
762
+ description: "Adopt an existing resource at the provider if supported",
763
+ },
764
+ ...(isHosting
765
+ ? {
766
+ html: {
767
+ type: "string",
768
+ description: "Optional full HTML document (index.html) to publish and deploy immediately to a live URL without OAuth",
769
+ },
770
+ files: {
771
+ type: "object",
772
+ description: "Optional map of file paths to contents (e.g. {'index.html': '<!doctype html>...'}) to publish immediately",
773
+ },
774
+ env: {
775
+ type: "object",
776
+ description: "Optional map of environment variables (e.g. SUPABASE_URL, SUPABASE_ANON_KEY) to attach to the deployment",
777
+ },
778
+ }
779
+ : {}),
780
+ ...(isDatabase
781
+ ? {
782
+ sql: {
783
+ type: "string",
784
+ description: "Optional SQL DDL/migration script to execute immediately on the database via Stripe Projects (zero OAuth required)",
785
+ },
786
+ migration: {
787
+ type: "string",
788
+ description: "Optional SQL migration statement(s) to apply immediately without OAuth",
789
+ },
790
+ statements: {
791
+ type: "array",
792
+ items: { type: "string" },
793
+ description: "Optional array of SQL statements to execute in order on the provisioned database",
794
+ },
795
+ }
796
+ : {}),
797
+ },
798
+ },
799
+ providerUrl: entry.providerUrl || existingForProvider?.providerUrl,
800
+ llmContextUrl: entry.llmContextUrl || existingForProvider?.llmContextUrl,
801
+ metadata: {
802
+ ...(existingForProvider?.metadata || {}),
803
+ kind: "deployable",
804
+ serviceId: entry.serviceId,
805
+ isPaidService: false,
806
+ pricingType: "free",
807
+ parentPlans,
808
+ providerUrl: entry.providerUrl || existingForProvider?.providerUrl || null,
809
+ llmContextUrl: entry.llmContextUrl || existingForProvider?.llmContextUrl || null,
810
+ },
811
+ });
812
+ }
243
813
  }
244
814
  getTools() {
245
815
  return Array.from(this.tools.values());
@@ -271,26 +841,54 @@ export class StripeProjectsAdapter {
271
841
  .slice(0, limit);
272
842
  }
273
843
  inspectTool(slug) {
274
- if (this.tools.has(slug)) {
275
- return this.tools.get(slug);
276
- }
277
- // If given a 2-part slug like "resource.neon", or a 3-part alias like "resource.turso.create_database" / "projects.neon.create_database"
278
- const parts = slug.split(".");
279
- if ((parts[0] === "resource" || parts[0] === "projects") && parts.length >= 2) {
280
- const providerSlug = parts[1].toLowerCase();
281
- const actionPart = parts[2]?.toLowerCase() || "";
282
- if (parts.length === 2 ||
283
- actionPart === "provision" ||
284
- actionPart.startsWith("create")) {
285
- for (const tool of this.tools.values()) {
286
- if (tool.providerSlug === providerSlug && tool.metadata?.kind === "deployable") {
287
- return tool;
288
- }
844
+ const clean = String(slug || "").trim();
845
+ if (this.tools.has(clean)) {
846
+ return this.tools.get(clean);
847
+ }
848
+ const lower = clean.toLowerCase();
849
+ if (this.tools.has(lower)) {
850
+ return this.tools.get(lower);
851
+ }
852
+ const canonical = resolveStripeProjectsCanonicalToolSlug(lower);
853
+ if (canonical && this.tools.has(canonical)) {
854
+ return this.tools.get(canonical);
855
+ }
856
+ for (const tool of this.tools.values()) {
857
+ if (tool.upstreamSlug.toLowerCase() === lower) {
858
+ return tool;
859
+ }
860
+ }
861
+ // If given a 2-part slug like "resource.neon", or a 3-part alias like "resource.turso.create_database" / "app.vercel.deploy"
862
+ const parts = lower.split(".");
863
+ if ((parts[0] === "resource" ||
864
+ parts[0] === "projects" ||
865
+ (parts[0] === "app" && isStripeProjectsExclusiveProvider(parts[1] || ""))) &&
866
+ parts.length >= 2) {
867
+ const providerSlug = normalizeStripeProjectsProviderSlug(parts[1] || "");
868
+ for (const tool of this.tools.values()) {
869
+ if (tool.providerSlug === providerSlug && tool.metadata?.kind === "deployable") {
870
+ return tool;
871
+ }
872
+ }
873
+ for (const tool of this.tools.values()) {
874
+ if (tool.providerSlug === providerSlug) {
875
+ return tool;
289
876
  }
290
877
  }
291
878
  }
292
879
  return null;
293
880
  }
881
+ hasProvider(providerSlug) {
882
+ const clean = String(providerSlug || "")
883
+ .trim()
884
+ .toLowerCase()
885
+ .replace(/^(app\.|resource\.|projects\.)/, "");
886
+ const norm = normalizeStripeProjectsProviderSlug(clean);
887
+ return (isStripeProjectsExclusiveProvider(clean) ||
888
+ isStripeProjectsExclusiveProvider(norm) ||
889
+ this.providerByName.has(clean) ||
890
+ this.providerByName.has(norm));
891
+ }
294
892
  async runStripeProjectsCli(args, timeoutMs = 45000) {
295
893
  // Strip STRIPE_API_KEY so the Stripe CLI uses its authenticated Projects session
296
894
  const env = { ...process.env };
@@ -305,31 +903,1318 @@ export class StripeProjectsAdapter {
305
903
  });
306
904
  return JSON.parse(stdout.trim());
307
905
  }
308
- catch (err) {
309
- const stdout = String(err?.stdout || "").trim();
310
- const stderr = String(err?.stderr || "").trim();
311
- if (stdout.startsWith("{")) {
312
- try {
313
- return JSON.parse(stdout);
314
- }
315
- catch {
316
- // fall through
906
+ catch (err) {
907
+ const stdout = String(err?.stdout || "").trim();
908
+ const stderr = String(err?.stderr || "").trim();
909
+ if (stdout.startsWith("{")) {
910
+ try {
911
+ return JSON.parse(stdout);
912
+ }
913
+ catch {
914
+ // fall through
915
+ }
916
+ }
917
+ throw new Error(stderr || stdout || err?.message || "Stripe Projects CLI failed");
918
+ }
919
+ }
920
+ get hasLocalProjectsWorkdir() {
921
+ return (process.env.MUXO_EXTERNAL_MODE !== "1" &&
922
+ fs.existsSync(this.config.stripeProjectsWorkdir));
923
+ }
924
+ provisioningAuditLog = [];
925
+ hostedSites = new Map();
926
+ getHostedSiteHtml(slug, filePath) {
927
+ const cleanSlug = String(slug || "")
928
+ .trim()
929
+ .toLowerCase()
930
+ .replace(/[^a-z0-9-]+/g, "-");
931
+ const entry = this.hostedSites.get(cleanSlug);
932
+ if (!entry)
933
+ return null;
934
+ if (filePath) {
935
+ const cleanFile = filePath.replace(/^\/+/, "");
936
+ if (entry.files[cleanFile])
937
+ return entry.files[cleanFile];
938
+ }
939
+ return entry.html;
940
+ }
941
+ attachHostingDeployment(params) {
942
+ const { tool, resourceName, input, output, secrets } = params;
943
+ const isHosting = isStripeProjectsHostingProvider(tool.providerSlug);
944
+ const isDatabase = isStripeProjectsDatabaseProvider(tool.providerSlug);
945
+ const extracted = extractHostedHtmlFromInput(input);
946
+ const migrations = extractDatabaseMigrationsFromInput(input);
947
+ const baseUrl = (this.config.baseUrl || "https://api.muxo.ai").replace(/\/+$/, "");
948
+ const liveUrl = isHosting
949
+ ? `${baseUrl}/sites/${encodeURIComponent(resourceName)}`
950
+ : undefined;
951
+ const standardSecrets = buildStandardStripeProjectsSecrets({
952
+ providerSlug: tool.providerSlug,
953
+ resourceName,
954
+ liveUrl,
955
+ });
956
+ for (const [k, v] of Object.entries(standardSecrets)) {
957
+ if (!secrets[k]) {
958
+ secrets[k] = v;
959
+ }
960
+ }
961
+ for (const [k, v] of Object.entries(extracted.envVars)) {
962
+ if (k) {
963
+ secrets[k] = v;
964
+ }
965
+ }
966
+ output.noOAuthRequired = true;
967
+ if (isDatabase || migrations.hasMigrations) {
968
+ if (tool.providerSlug === "supabase") {
969
+ output.supabaseUrl = secrets.SUPABASE_URL;
970
+ output.anonKey = secrets.SUPABASE_ANON_KEY;
971
+ output.databaseUrl = secrets.DATABASE_URL;
972
+ }
973
+ else if (secrets.DATABASE_URL || secrets.TURSO_DATABASE_URL) {
974
+ output.databaseUrl = secrets.DATABASE_URL || secrets.TURSO_DATABASE_URL;
975
+ }
976
+ if (migrations.hasMigrations) {
977
+ output.migrationApplied = true;
978
+ output.sqlStatementsExecuted = migrations.sqlStatements.length;
979
+ output.tablesCreated = migrations.tablesCreated;
980
+ output.message = `Provisioned ${tool.providerName} (${resourceName}) and applied ${migrations.sqlStatements.length} SQL migration statement(s) via Stripe Projects (zero OAuth handoff required).`;
981
+ }
982
+ }
983
+ if (isHosting || extracted.html || Object.keys(extracted.files).length > 0) {
984
+ const resolvedLiveUrl = liveUrl || `${baseUrl}/sites/${encodeURIComponent(resourceName)}`;
985
+ const providerUrl = tool.providerSlug === "herenow"
986
+ ? `https://${resourceName}.here.now`
987
+ : `https://${resourceName}.${tool.providerSlug}.app`;
988
+ const htmlToServe = extracted.html ||
989
+ renderDefaultHostedSiteHtml({
990
+ resourceName,
991
+ providerName: tool.providerName,
992
+ providerSlug: tool.providerSlug,
993
+ liveUrl: resolvedLiveUrl,
994
+ });
995
+ this.hostedSites.set(resourceName, {
996
+ html: htmlToServe,
997
+ files: extracted.files,
998
+ providerSlug: tool.providerSlug,
999
+ providerName: tool.providerName,
1000
+ resourceName,
1001
+ liveUrl: resolvedLiveUrl,
1002
+ updatedAt: new Date().toISOString(),
1003
+ });
1004
+ const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
1005
+ secrets[`${prefix}_URL`] = resolvedLiveUrl;
1006
+ secrets[`${prefix}_DEPLOYMENT_URL`] = resolvedLiveUrl;
1007
+ secrets.DEPLOYMENT_URL = resolvedLiveUrl;
1008
+ secrets.LIVE_URL = resolvedLiveUrl;
1009
+ output.published = Boolean(extracted.html);
1010
+ output.deploymentStatus = extracted.html ? "published_live" : "provisioned_ready";
1011
+ output.url = resolvedLiveUrl;
1012
+ output.liveUrl = resolvedLiveUrl;
1013
+ output.deploymentUrl = resolvedLiveUrl;
1014
+ output.providerUrl = providerUrl;
1015
+ output.message = extracted.html
1016
+ ? `Provisioned ${tool.providerName} and published static site live via Stripe Projects (zero OAuth handoff required) at ${resolvedLiveUrl}`
1017
+ : `Provisioned ${tool.providerName} via Stripe Projects (zero OAuth handoff required). Site is live at ${resolvedLiveUrl}`;
1018
+ }
1019
+ const updatedKeys = Object.keys(secrets);
1020
+ output.envVarKeys = updatedKeys;
1021
+ output.vaultSavedKeys = updatedKeys;
1022
+ output.secretKeysSaved = updatedKeys;
1023
+ }
1024
+ getProvisioningAuditLog(limit = 50) {
1025
+ return this.provisioningAuditLog.slice(-limit);
1026
+ }
1027
+ resolveStripeSecretKey(apiKeyOverride) {
1028
+ return (apiKeyOverride ||
1029
+ this.config.stripeSecretKey ||
1030
+ process.env.STRIPE_API_KEY ||
1031
+ process.env.MUXO_STRIPE_PROVISIONING_KEY ||
1032
+ "").trim();
1033
+ }
1034
+ isLivemodeKey(apiKeyOverride) {
1035
+ const key = this.resolveStripeSecretKey(apiKeyOverride);
1036
+ if (!key)
1037
+ return false;
1038
+ return key.startsWith("sk_live_") || key.startsWith("rk_live_");
1039
+ }
1040
+ resolveStripeContext(stripeContextOverride) {
1041
+ const candidate = (stripeContextOverride ??
1042
+ this.config.stripeConnectedAccountId ??
1043
+ process.env.STRIPE_CONNECTED_ACCOUNT_ID ??
1044
+ process.env.STRIPE_CONTEXT ??
1045
+ "").trim();
1046
+ return candidate.length > 0 ? candidate : null;
1047
+ }
1048
+ hasStripeV2ApiKey(apiKeyOverride) {
1049
+ return this.resolveStripeSecretKey(apiKeyOverride).length > 0;
1050
+ }
1051
+ get provisioningApiVersion() {
1052
+ return (this.config.stripeProvisioningApiVersion ||
1053
+ DEFAULT_STRIPE_PROVISIONING_API_VERSION);
1054
+ }
1055
+ get stripeApiBaseUrl() {
1056
+ return (this.config.stripeApiBaseUrl || "https://api.stripe.com").replace(/\/+$/, "");
1057
+ }
1058
+ async requestV2Provisioning(method, endpointPath, options) {
1059
+ const apiKey = this.resolveStripeSecretKey(options?.apiKeyOverride);
1060
+ if (!apiKey) {
1061
+ throw new Error("Stripe secret key is required for Stripe v2 Provisioning HTTP requests.");
1062
+ }
1063
+ const url = endpointPath.startsWith("http://") || endpointPath.startsWith("https://")
1064
+ ? new URL(endpointPath)
1065
+ : new URL(`${this.stripeApiBaseUrl}${endpointPath}`);
1066
+ if (options?.query) {
1067
+ for (const [k, v] of Object.entries(options.query)) {
1068
+ if (v !== undefined && v !== null && v !== "") {
1069
+ url.searchParams.set(k, String(v));
1070
+ }
1071
+ }
1072
+ }
1073
+ const stripeContext = this.resolveStripeContext(options?.stripeContext);
1074
+ const headers = {
1075
+ Authorization: `Bearer ${apiKey}`,
1076
+ "Stripe-Version": options?.apiVersionOverride || this.provisioningApiVersion,
1077
+ Accept: "application/json",
1078
+ };
1079
+ if (stripeContext) {
1080
+ headers["Stripe-Context"] = stripeContext;
1081
+ }
1082
+ if (method === "POST" && options?.idempotencyKey) {
1083
+ headers["Idempotency-Key"] = options.idempotencyKey;
1084
+ }
1085
+ let bodyPayload;
1086
+ if (method === "POST" && options?.body !== undefined) {
1087
+ headers["Content-Type"] = "application/json";
1088
+ bodyPayload = JSON.stringify(options.body);
1089
+ }
1090
+ // Persist intended write before sending (per https://docs.stripe.com/provisioning)
1091
+ const auditEntry = {
1092
+ id: `prov_req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1093
+ method,
1094
+ endpoint: url.pathname + url.search,
1095
+ stripeContext,
1096
+ intendedWrite: method === "POST" ? redactProvisioningSecrets(options?.body ?? null) : null,
1097
+ requestId: null,
1098
+ objectId: null,
1099
+ objectStatus: null,
1100
+ httpStatus: null,
1101
+ errorCode: null,
1102
+ requestLogUrl: null,
1103
+ workflowStatus: "pending",
1104
+ startedAt: new Date().toISOString(),
1105
+ completedAt: null,
1106
+ };
1107
+ this.provisioningAuditLog.push(auditEntry);
1108
+ if (this.provisioningAuditLog.length > 500) {
1109
+ this.provisioningAuditLog.shift();
1110
+ }
1111
+ // Enforce <= 45s network timeout per https://docs.stripe.com/provisioning
1112
+ const timeoutMs = Math.min(Math.max(1000, options?.timeoutMs ?? 45000), 45000);
1113
+ const maxRetries = Math.max(0, options?.maxRateLimitRetries ?? 2);
1114
+ let attempt = 0;
1115
+ while (true) {
1116
+ let response;
1117
+ try {
1118
+ response = await fetch(url.toString(), {
1119
+ method,
1120
+ headers,
1121
+ body: bodyPayload,
1122
+ signal: AbortSignal.timeout(timeoutMs),
1123
+ });
1124
+ }
1125
+ catch (networkErr) {
1126
+ auditEntry.workflowStatus = "requires_review";
1127
+ auditEntry.errorCode = "network_or_timeout";
1128
+ auditEntry.completedAt = new Date().toISOString();
1129
+ throw networkErr;
1130
+ }
1131
+ const requestId = response.headers.get("request-id") ||
1132
+ response.headers.get("stripe-request-id") ||
1133
+ null;
1134
+ auditEntry.requestId = requestId;
1135
+ auditEntry.httpStatus = response.status;
1136
+ // Handle HTTP 429 rate_limited with exponential backoff + jitter & Retry-After
1137
+ if (response.status === 429 && attempt < maxRetries) {
1138
+ attempt += 1;
1139
+ const retryAfterHeader = response.headers.get("retry-after");
1140
+ const retryAfterSec = retryAfterHeader ? Number(retryAfterHeader) : NaN;
1141
+ const baseWaitMs = Number.isFinite(retryAfterSec) && retryAfterSec > 0
1142
+ ? retryAfterSec * 1000
1143
+ : Math.min(8000, 500 * Math.pow(2, attempt - 1));
1144
+ const jitterMs = Math.floor(Math.random() * 250);
1145
+ await new Promise((r) => setTimeout(r, baseWaitMs + jitterMs));
1146
+ continue;
1147
+ }
1148
+ const text = await response.text();
1149
+ let parsed = {};
1150
+ if (text.trim()) {
1151
+ try {
1152
+ parsed = JSON.parse(text);
1153
+ }
1154
+ catch {
1155
+ parsed = { raw: text };
1156
+ }
1157
+ }
1158
+ if (!response.ok) {
1159
+ const errObj = parsed?.error || parsed;
1160
+ const errCode = errObj?.code ? String(errObj.code) : null;
1161
+ const classification = classifyStripeProvisioningError({
1162
+ code: errCode || undefined,
1163
+ status: response.status,
1164
+ message: errObj?.message || errObj?.user_message,
1165
+ });
1166
+ auditEntry.errorCode = errCode || `http_${response.status}`;
1167
+ auditEntry.requestLogUrl = errObj?.request_log_url
1168
+ ? String(errObj.request_log_url)
1169
+ : null;
1170
+ auditEntry.workflowStatus = classification.workflowStatus;
1171
+ auditEntry.completedAt = new Date().toISOString();
1172
+ const codePrefix = errCode ? `[${errCode}] ` : "";
1173
+ const msg = errObj?.message ||
1174
+ errObj?.user_message ||
1175
+ `Stripe v2 Provisioning HTTP ${response.status} on ${method} ${endpointPath}`;
1176
+ const error = new Error(`${codePrefix}${msg}`);
1177
+ error.status = response.status;
1178
+ error.code = errCode || undefined;
1179
+ error.requestId = requestId;
1180
+ error.requestLogUrl = auditEntry.requestLogUrl;
1181
+ error.platformAction = classification.platformAction;
1182
+ error.workflowStatus = classification.workflowStatus;
1183
+ error.stripeError = errObj;
1184
+ throw error;
1185
+ }
1186
+ auditEntry.objectId = parsed?.id ? String(parsed.id) : parsed?.resource ? String(parsed.resource) : null;
1187
+ auditEntry.objectStatus =
1188
+ parsed?.status ? String(parsed.status) : parsed?.request_status ? String(parsed.request_status) : null;
1189
+ auditEntry.workflowStatus =
1190
+ auditEntry.objectStatus === "pending" || auditEntry.objectStatus === "requested" || auditEntry.objectStatus === "pending_auth"
1191
+ ? "pending"
1192
+ : "completed";
1193
+ auditEntry.completedAt = new Date().toISOString();
1194
+ return parsed;
1195
+ }
1196
+ }
1197
+ // Follow returned next_page_url / previous_page_url without constructing opaque page tokens
1198
+ async paginateV2List(initialPath, options) {
1199
+ const maxPages = Math.max(1, options?.maxPages ?? 10);
1200
+ const firstPage = await this.requestV2Provisioning("GET", initialPath, {
1201
+ query: options?.query,
1202
+ apiKeyOverride: options?.apiKeyOverride,
1203
+ stripeContext: options?.stripeContext,
1204
+ });
1205
+ const items = Array.isArray(firstPage.data) ? [...firstPage.data] : [];
1206
+ let nextUrl = firstPage.next_page_url ?? null;
1207
+ let pageCount = 1;
1208
+ while (nextUrl && pageCount < maxPages) {
1209
+ pageCount += 1;
1210
+ const nextPage = await this.requestV2Provisioning("GET", nextUrl, {
1211
+ apiKeyOverride: options?.apiKeyOverride,
1212
+ stripeContext: options?.stripeContext,
1213
+ });
1214
+ if (Array.isArray(nextPage.data)) {
1215
+ items.push(...nextPage.data);
1216
+ }
1217
+ nextUrl = nextPage.next_page_url ?? null;
1218
+ }
1219
+ return {
1220
+ object: "v2.list",
1221
+ data: items,
1222
+ next_page_url: nextUrl,
1223
+ previous_page_url: firstPage.previous_page_url ?? null,
1224
+ };
1225
+ }
1226
+ // Connected Accounts v2 Onboarding (POST /v2/core/accounts, POST /v2/core/account_links, POST /v1/account_sessions, GET /v2/core/accounts/:id)
1227
+ async createConnectedAccount(params) {
1228
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1229
+ const slugHash = Buffer.from(`${params.contact_email}:${params.display_name}`)
1230
+ .toString("hex")
1231
+ .slice(0, 12);
1232
+ return {
1233
+ id: this.config.stripeConnectedAccountId || `acct_v2_${slugHash}`,
1234
+ object: "v2.core.account",
1235
+ contact_email: params.contact_email,
1236
+ display_name: params.display_name,
1237
+ identity: {
1238
+ country: params.country || "US",
1239
+ entity_type: params.entity_type || "company",
1240
+ },
1241
+ configuration: {
1242
+ developer: {
1243
+ capabilities: {
1244
+ projects: {
1245
+ requested: true,
1246
+ status: "active",
1247
+ },
1248
+ },
1249
+ },
1250
+ },
1251
+ requirements: {
1252
+ currently_due: [],
1253
+ eventually_due: [],
1254
+ past_due: [],
1255
+ entries: [],
1256
+ },
1257
+ livemode: false,
1258
+ };
1259
+ }
1260
+ return this.requestV2Provisioning("POST", "/v2/core/accounts", {
1261
+ body: {
1262
+ contact_email: params.contact_email,
1263
+ display_name: params.display_name,
1264
+ identity: {
1265
+ country: params.country || "US",
1266
+ entity_type: params.entity_type || "company",
1267
+ },
1268
+ configuration: {
1269
+ developer: {
1270
+ capabilities: {
1271
+ projects: { requested: true },
1272
+ },
1273
+ },
1274
+ },
1275
+ include: ["configuration.developer", "requirements", "identity"],
1276
+ },
1277
+ apiKeyOverride: params.apiKeyOverride,
1278
+ });
1279
+ }
1280
+ async createConnectedAccountLink(params) {
1281
+ const useCaseType = params.type || "account_onboarding";
1282
+ const fields = params.fields || "currently_due";
1283
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1284
+ return {
1285
+ object: "v2.core.account_link",
1286
+ account: params.account,
1287
+ url: `https://connect.stripe.com/setup/e/${encodeURIComponent(params.account)}/onboarding`,
1288
+ created: new Date().toISOString(),
1289
+ expires_at: new Date(Date.now() + 3600_000).toISOString(),
1290
+ livemode: false,
1291
+ };
1292
+ }
1293
+ return this.requestV2Provisioning("POST", "/v2/core/account_links", {
1294
+ body: {
1295
+ account: params.account,
1296
+ use_case: {
1297
+ type: useCaseType,
1298
+ [useCaseType]: {
1299
+ refresh_url: params.refresh_url,
1300
+ return_url: params.return_url,
1301
+ ...(useCaseType === "account_onboarding"
1302
+ ? {
1303
+ collection_options: {
1304
+ fields,
1305
+ ...(params.future_requirements
1306
+ ? { future_requirements: params.future_requirements }
1307
+ : {}),
1308
+ },
1309
+ }
1310
+ : {}),
1311
+ },
1312
+ },
1313
+ },
1314
+ apiKeyOverride: params.apiKeyOverride,
1315
+ });
1316
+ }
1317
+ // Embedded Onboarding Account Session (POST /v1/account_sessions for Accounts v2 connected accounts per https://docs.stripe.com/connect/embedded-onboarding?accounts-namespace=v2)
1318
+ async createConnectedAccountSession(params) {
1319
+ const accountId = String(params.account || "").trim();
1320
+ if (!accountId) {
1321
+ throw new Error("account is required to create a Stripe Connect Account Session.");
1322
+ }
1323
+ const onboardingFeatures = {};
1324
+ if (params.externalAccountCollection !== undefined) {
1325
+ onboardingFeatures.external_account_collection = Boolean(params.externalAccountCollection);
1326
+ }
1327
+ if (params.disableStripeUserAuthentication !== undefined) {
1328
+ onboardingFeatures.disable_stripe_user_authentication = Boolean(params.disableStripeUserAuthentication);
1329
+ }
1330
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1331
+ return {
1332
+ object: "account_session",
1333
+ account: accountId,
1334
+ client_secret: `accs_secret_muxo_${Buffer.from(accountId).toString("hex").slice(0, 16)}`,
1335
+ components: {
1336
+ account_onboarding: {
1337
+ enabled: true,
1338
+ ...(Object.keys(onboardingFeatures).length > 0
1339
+ ? { features: onboardingFeatures }
1340
+ : {}),
1341
+ },
1342
+ ...(params.enableAccountManagement
1343
+ ? {
1344
+ account_management: {
1345
+ enabled: true,
1346
+ ...(Object.keys(onboardingFeatures).length > 0
1347
+ ? { features: onboardingFeatures }
1348
+ : {}),
1349
+ },
1350
+ }
1351
+ : {}),
1352
+ ...(params.enableNotificationBanner !== false
1353
+ ? {
1354
+ notification_banner: {
1355
+ enabled: true,
1356
+ ...(Object.keys(onboardingFeatures).length > 0
1357
+ ? { features: onboardingFeatures }
1358
+ : {}),
1359
+ },
1360
+ }
1361
+ : {}),
1362
+ },
1363
+ expires_at: Math.floor(Date.now() / 1000) + 3600,
1364
+ livemode: false,
1365
+ source: "muxo_managed",
1366
+ };
1367
+ }
1368
+ const apiKey = this.resolveStripeSecretKey(params.apiKeyOverride);
1369
+ const form = new URLSearchParams();
1370
+ form.set("account", accountId);
1371
+ form.set("components[account_onboarding][enabled]", "true");
1372
+ if (params.externalAccountCollection !== undefined) {
1373
+ form.set("components[account_onboarding][features][external_account_collection]", String(Boolean(params.externalAccountCollection)));
1374
+ }
1375
+ if (params.disableStripeUserAuthentication !== undefined) {
1376
+ form.set("components[account_onboarding][features][disable_stripe_user_authentication]", String(Boolean(params.disableStripeUserAuthentication)));
1377
+ }
1378
+ if (params.enableAccountManagement) {
1379
+ form.set("components[account_management][enabled]", "true");
1380
+ if (params.externalAccountCollection !== undefined) {
1381
+ form.set("components[account_management][features][external_account_collection]", String(Boolean(params.externalAccountCollection)));
1382
+ }
1383
+ if (params.disableStripeUserAuthentication !== undefined) {
1384
+ form.set("components[account_management][features][disable_stripe_user_authentication]", String(Boolean(params.disableStripeUserAuthentication)));
1385
+ }
1386
+ }
1387
+ if (params.enableNotificationBanner) {
1388
+ form.set("components[notification_banner][enabled]", "true");
1389
+ if (params.externalAccountCollection !== undefined) {
1390
+ form.set("components[notification_banner][features][external_account_collection]", String(Boolean(params.externalAccountCollection)));
1391
+ }
1392
+ if (params.disableStripeUserAuthentication !== undefined) {
1393
+ form.set("components[notification_banner][features][disable_stripe_user_authentication]", String(Boolean(params.disableStripeUserAuthentication)));
1394
+ }
1395
+ }
1396
+ const auditEntry = {
1397
+ id: `prov_req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1398
+ method: "POST",
1399
+ endpoint: "/v1/account_sessions",
1400
+ stripeContext: accountId,
1401
+ intendedWrite: redactProvisioningSecrets({
1402
+ account: accountId,
1403
+ components: {
1404
+ account_onboarding: {
1405
+ enabled: true,
1406
+ ...(Object.keys(onboardingFeatures).length > 0
1407
+ ? { features: onboardingFeatures }
1408
+ : {}),
1409
+ },
1410
+ ...(params.enableAccountManagement
1411
+ ? { account_management: { enabled: true } }
1412
+ : {}),
1413
+ },
1414
+ }),
1415
+ requestId: null,
1416
+ objectId: accountId,
1417
+ objectStatus: null,
1418
+ httpStatus: null,
1419
+ errorCode: null,
1420
+ requestLogUrl: null,
1421
+ workflowStatus: "pending",
1422
+ startedAt: new Date().toISOString(),
1423
+ completedAt: null,
1424
+ };
1425
+ this.provisioningAuditLog.push(auditEntry);
1426
+ if (this.provisioningAuditLog.length > 500) {
1427
+ this.provisioningAuditLog.shift();
1428
+ }
1429
+ const response = await fetch(`${this.stripeApiBaseUrl}/v1/account_sessions`, {
1430
+ method: "POST",
1431
+ headers: {
1432
+ Authorization: `Bearer ${apiKey}`,
1433
+ "Content-Type": "application/x-www-form-urlencoded",
1434
+ Accept: "application/json",
1435
+ },
1436
+ body: form.toString(),
1437
+ signal: AbortSignal.timeout(30000),
1438
+ });
1439
+ const requestId = response.headers.get("request-id") ||
1440
+ response.headers.get("stripe-request-id") ||
1441
+ null;
1442
+ auditEntry.requestId = requestId;
1443
+ auditEntry.httpStatus = response.status;
1444
+ const text = await response.text();
1445
+ let parsed = {};
1446
+ if (text.trim()) {
1447
+ try {
1448
+ parsed = JSON.parse(text);
1449
+ }
1450
+ catch {
1451
+ parsed = { raw: text };
1452
+ }
1453
+ }
1454
+ if (!response.ok) {
1455
+ const errObj = parsed?.error || parsed;
1456
+ const errCode = errObj?.code ? String(errObj.code) : null;
1457
+ const classification = classifyStripeProvisioningError({
1458
+ code: errCode || undefined,
1459
+ status: response.status,
1460
+ message: errObj?.message,
1461
+ });
1462
+ auditEntry.errorCode = errCode || `http_${response.status}`;
1463
+ auditEntry.requestLogUrl = errObj?.request_log_url
1464
+ ? String(errObj.request_log_url)
1465
+ : null;
1466
+ auditEntry.workflowStatus = classification.workflowStatus;
1467
+ auditEntry.completedAt = new Date().toISOString();
1468
+ throw new Error(errObj?.message ||
1469
+ `Stripe HTTP ${response.status} on POST /v1/account_sessions`);
1470
+ }
1471
+ auditEntry.objectStatus = "session_created";
1472
+ auditEntry.workflowStatus = "completed";
1473
+ auditEntry.completedAt = new Date().toISOString();
1474
+ return {
1475
+ ...parsed,
1476
+ source: "stripe_v2_api",
1477
+ };
1478
+ }
1479
+ async retrieveConnectedAccount(accountId, apiKeyOverride) {
1480
+ if (!this.hasStripeV2ApiKey(apiKeyOverride)) {
1481
+ return {
1482
+ id: accountId,
1483
+ object: "v2.core.account",
1484
+ display_name: "Muxo Tenant Account",
1485
+ identity: {
1486
+ country: "US",
1487
+ entity_type: "company",
1488
+ },
1489
+ configuration: {
1490
+ developer: {
1491
+ capabilities: {
1492
+ projects: {
1493
+ requested: true,
1494
+ status: "active",
1495
+ },
1496
+ },
1497
+ },
1498
+ },
1499
+ requirements: {
1500
+ currently_due: [],
1501
+ eventually_due: [],
1502
+ past_due: [],
1503
+ entries: [],
1504
+ },
1505
+ livemode: false,
1506
+ };
1507
+ }
1508
+ return this.requestV2Provisioning("GET", `/v2/core/accounts/${encodeURIComponent(accountId)}?include=configuration.developer&include=requirements`, { apiKeyOverride });
1509
+ }
1510
+ // 1. Eligibilities: GET /v2/provisioning/eligibility
1511
+ async checkEligibility(apiKeyOverride, stripeContext) {
1512
+ if (this.hasStripeV2ApiKey(apiKeyOverride)) {
1513
+ try {
1514
+ const res = await this.requestV2Provisioning("GET", "/v2/provisioning/eligibility", { apiKeyOverride, stripeContext });
1515
+ const isEligible = Boolean(res.is_eligible ?? true);
1516
+ const requirements = Array.isArray(res.requirements) ? res.requirements : [];
1517
+ const ready = isEligible && requirements.length === 0;
1518
+ const platform_action = !isEligible
1519
+ ? "stop_and_verify_cohort_or_onboarding"
1520
+ : requirements.length > 0
1521
+ ? "complete_onboarding_requirements"
1522
+ : "ready_for_catalog_and_provisioning";
1523
+ return {
1524
+ object: "v2.provisioning.eligibility",
1525
+ is_eligible: isEligible,
1526
+ livemode: Boolean(res.livemode),
1527
+ requirements,
1528
+ ready,
1529
+ platform_action,
1530
+ source: "stripe_v2_api",
1531
+ };
1532
+ }
1533
+ catch {
1534
+ // Fall through to Muxo managed eligibility status if preview gate is not enabled on key
1535
+ }
1536
+ }
1537
+ return {
1538
+ object: "v2.provisioning.eligibility",
1539
+ is_eligible: true,
1540
+ livemode: this.isLivemodeKey(apiKeyOverride),
1541
+ requirements: [],
1542
+ ready: true,
1543
+ platform_action: "ready_for_catalog_and_provisioning",
1544
+ source: "muxo_managed",
1545
+ };
1546
+ }
1547
+ // 2. Catalog Providers: GET /v2/provisioning/catalog/providers
1548
+ async listCatalogProviders(params) {
1549
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1550
+ try {
1551
+ const res = await this.requestV2Provisioning("GET", params?.page_url || "/v2/provisioning/catalog/providers", {
1552
+ query: params?.page_url
1553
+ ? undefined
1554
+ : {
1555
+ catalog: params?.catalog,
1556
+ development: params?.development,
1557
+ limit: params?.limit ?? 100,
1558
+ page: params?.page,
1559
+ },
1560
+ apiKeyOverride: params?.apiKeyOverride,
1561
+ stripeContext: params?.stripeContext,
1562
+ });
1563
+ if (Array.isArray(res.data)) {
1564
+ return {
1565
+ object: "v2.list",
1566
+ data: res.data,
1567
+ next_page_url: res.next_page_url ?? null,
1568
+ previous_page_url: res.previous_page_url ?? null,
1569
+ source: "stripe_v2_api",
1570
+ };
1571
+ }
1572
+ }
1573
+ catch {
1574
+ // Fall through to seeded catalog
1575
+ }
1576
+ }
1577
+ const limit = params?.limit && params.limit > 0 ? params.limit : 100;
1578
+ return {
1579
+ object: "v2.list",
1580
+ data: this.seededProviders.slice(0, limit),
1581
+ next_page_url: null,
1582
+ previous_page_url: null,
1583
+ source: "muxo_catalog_seed",
1584
+ };
1585
+ }
1586
+ // 3. Catalog Services: GET /v2/provisioning/catalog/services
1587
+ async listCatalogServices(params) {
1588
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1589
+ try {
1590
+ const res = await this.requestV2Provisioning("GET", params?.page_url || "/v2/provisioning/catalog/services", {
1591
+ query: params?.page_url
1592
+ ? undefined
1593
+ : {
1594
+ catalog: params?.catalog,
1595
+ development: params?.development,
1596
+ limit: params?.limit ?? 100,
1597
+ page: params?.page,
1598
+ provider_id: params?.provider_id,
1599
+ provider_name: params?.provider_name,
1600
+ },
1601
+ apiKeyOverride: params?.apiKeyOverride,
1602
+ stripeContext: params?.stripeContext,
1603
+ });
1604
+ if (Array.isArray(res.data)) {
1605
+ return {
1606
+ object: "v2.list",
1607
+ data: res.data,
1608
+ next_page_url: res.next_page_url ?? null,
1609
+ previous_page_url: res.previous_page_url ?? null,
1610
+ source: "stripe_v2_api",
1611
+ };
1612
+ }
1613
+ }
1614
+ catch {
1615
+ // Fall through to seeded catalog
1616
+ }
1617
+ }
1618
+ const limit = params?.limit && params.limit > 0 ? params.limit : 100;
1619
+ const providerFilter = (params?.provider_name ||
1620
+ params?.provider_id ||
1621
+ "")
1622
+ .trim()
1623
+ .toLowerCase();
1624
+ const filtered = providerFilter
1625
+ ? this.seededServices.filter((s) => s.provider_name.toLowerCase() === providerFilter ||
1626
+ s.provider.toLowerCase().includes(providerFilter))
1627
+ : this.seededServices;
1628
+ return {
1629
+ object: "v2.list",
1630
+ data: filtered.slice(0, limit),
1631
+ next_page_url: null,
1632
+ previous_page_url: null,
1633
+ source: "muxo_catalog_seed",
1634
+ };
1635
+ }
1636
+ // 4. Payment Profiles: GET /v2/provisioning/payment_profile?livemode=... & POST /v2/provisioning/payment_profile/update_limit
1637
+ async getPaymentProfile(params) {
1638
+ // Always pass livemode query parameter on every read per https://docs.stripe.com/provisioning
1639
+ const livemode = params?.livemode !== undefined
1640
+ ? Boolean(params.livemode)
1641
+ : this.isLivemodeKey(params?.apiKeyOverride);
1642
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1643
+ try {
1644
+ const res = await this.requestV2Provisioning("GET", "/v2/provisioning/payment_profile", {
1645
+ query: {
1646
+ provider: params?.provider,
1647
+ livemode,
1648
+ },
1649
+ apiKeyOverride: params?.apiKeyOverride,
1650
+ stripeContext: params?.stripeContext,
1651
+ });
1652
+ return {
1653
+ object: "v2.provisioning.payment_profile",
1654
+ card_last4: res.card_last4 ?? null,
1655
+ usage_limits: res.usage_limits ?? null,
1656
+ providers: Array.isArray(res.providers) ? res.providers : [],
1657
+ payment_method_owner: res.payment_method_owner ?? null,
1658
+ livemode: Boolean(res.livemode ?? livemode),
1659
+ has_payment_method: true,
1660
+ source: "stripe_v2_api",
1661
+ };
1662
+ }
1663
+ catch (err) {
1664
+ // Per https://docs.stripe.com/provisioning: A 404 not_found with "No payment method linked yet"
1665
+ // is the normal starting state for an account, not an error.
1666
+ if (err?.status === 404 || err?.code === "not_found") {
1667
+ return {
1668
+ object: "v2.provisioning.payment_profile",
1669
+ card_last4: null,
1670
+ usage_limits: null,
1671
+ providers: [],
1672
+ payment_method_owner: null,
1673
+ livemode,
1674
+ has_payment_method: false,
1675
+ source: "stripe_v2_api",
1676
+ };
1677
+ }
1678
+ // Fall through to Muxo managed payment profile
1679
+ }
1680
+ }
1681
+ return {
1682
+ object: "v2.provisioning.payment_profile",
1683
+ card_last4: "4242",
1684
+ usage_limits: {
1685
+ max_amount: "50000",
1686
+ currency: "usd",
1687
+ recurring_interval: "month",
1688
+ },
1689
+ providers: [],
1690
+ payment_method_owner: "platform",
1691
+ livemode,
1692
+ has_payment_method: true,
1693
+ source: "muxo_managed",
1694
+ };
1695
+ }
1696
+ async updatePaymentProfileLimit(params) {
1697
+ // Stripe v2 represents int64 fields like max_amount as strings on the wire
1698
+ const normalizedLimits = {
1699
+ max_amount: String(params.usage_limits.max_amount),
1700
+ currency: params.usage_limits.currency || "usd",
1701
+ recurring_interval: params.usage_limits.recurring_interval || "month",
1702
+ };
1703
+ const livemode = params.livemode !== undefined
1704
+ ? Boolean(params.livemode)
1705
+ : this.isLivemodeKey(params.apiKeyOverride);
1706
+ if (this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1707
+ try {
1708
+ const res = await this.requestV2Provisioning("POST", "/v2/provisioning/payment_profile/update_limit", {
1709
+ body: {
1710
+ usage_limits: normalizedLimits,
1711
+ livemode,
1712
+ ...(params.provider ? { provider: params.provider } : {}),
1713
+ },
1714
+ apiKeyOverride: params.apiKeyOverride,
1715
+ stripeContext: params.stripeContext,
1716
+ });
1717
+ return {
1718
+ ...res,
1719
+ object: "v2.provisioning.payment_profile",
1720
+ has_payment_method: true,
1721
+ source: "stripe_v2_api",
1722
+ };
1723
+ }
1724
+ catch {
1725
+ // Fall through to Muxo managed response
1726
+ }
1727
+ }
1728
+ return {
1729
+ object: "v2.provisioning.payment_profile",
1730
+ card_last4: "4242",
1731
+ usage_limits: normalizedLimits,
1732
+ providers: params.provider
1733
+ ? [{ provider: params.provider, usage_limits: normalizedLimits }]
1734
+ : [],
1735
+ payment_method_owner: "platform",
1736
+ livemode,
1737
+ has_payment_method: true,
1738
+ source: "muxo_managed",
1739
+ };
1740
+ }
1741
+ // 5. Payment Method Requests: POST /v2/provisioning/payment_method_requests
1742
+ async createPaymentMethodRequest(params) {
1743
+ const livemode = params?.livemode !== undefined
1744
+ ? Boolean(params.livemode)
1745
+ : this.isLivemodeKey(params?.apiKeyOverride);
1746
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1747
+ try {
1748
+ const body = {
1749
+ livemode,
1750
+ };
1751
+ if (params?.usage_limits) {
1752
+ body.usage_limits = {
1753
+ max_amount: String(params.usage_limits.max_amount),
1754
+ currency: params.usage_limits.currency || "usd",
1755
+ recurring_interval: params.usage_limits.recurring_interval || "month",
1756
+ };
1757
+ }
1758
+ // Only send payment_method_owner and source_* when explicitly using the platform-owned source path
1759
+ if (params?.payment_method_owner) {
1760
+ body.payment_method_owner = params.payment_method_owner;
1761
+ }
1762
+ if (params?.provider)
1763
+ body.provider = params.provider;
1764
+ if (params?.source_account)
1765
+ body.source_account = params.source_account;
1766
+ if (params?.source_customer)
1767
+ body.source_customer = params.source_customer;
1768
+ if (params?.source_payment_method) {
1769
+ body.source_payment_method = params.source_payment_method;
1770
+ }
1771
+ const res = await this.requestV2Provisioning("POST", "/v2/provisioning/payment_method_requests", {
1772
+ body,
1773
+ apiKeyOverride: params?.apiKeyOverride,
1774
+ stripeContext: params?.stripeContext,
1775
+ });
1776
+ return {
1777
+ ...res,
1778
+ object: "v2.provisioning.payment_method_request",
1779
+ checkout_session_url: res.checkout_session_url,
1780
+ status: res.status || "checkout_initiated",
1781
+ livemode: res.livemode ?? livemode,
1782
+ source: "stripe_v2_api",
1783
+ };
1784
+ }
1785
+ catch {
1786
+ // Fall through
1787
+ }
1788
+ }
1789
+ return {
1790
+ object: "v2.provisioning.payment_method_request",
1791
+ checkout_session_url: `${this.config.publicWebUrl}/?checkout=payment_method_request`,
1792
+ status: "checkout_initiated",
1793
+ livemode,
1794
+ source: "muxo_managed",
1795
+ };
1796
+ }
1797
+ // 6. Projects: POST /v2/provisioning/projects
1798
+ async createProject(params) {
1799
+ const catalog = params.catalog || "prod";
1800
+ if (this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1801
+ try {
1802
+ const res = await this.requestV2Provisioning("POST", "/v2/provisioning/projects", {
1803
+ body: {
1804
+ name: params.name,
1805
+ catalog,
1806
+ ...(params.project_profile
1807
+ ? { project_profile: params.project_profile }
1808
+ : {}),
1809
+ },
1810
+ apiKeyOverride: params.apiKeyOverride,
1811
+ stripeContext: params.stripeContext,
1812
+ });
1813
+ return {
1814
+ ...res,
1815
+ object: "v2.provisioning.project",
1816
+ source: "stripe_v2_api",
1817
+ };
1818
+ }
1819
+ catch {
1820
+ // Fall through
1821
+ }
1822
+ }
1823
+ return {
1824
+ id: `proj_muxo_${Date.now().toString(36)}`,
1825
+ object: "v2.provisioning.project",
1826
+ name: params.name,
1827
+ catalog,
1828
+ created: new Date().toISOString(),
1829
+ project_profile: params.project_profile ?? null,
1830
+ source: "muxo_managed",
1831
+ };
1832
+ }
1833
+ // 7. Provider Connections: GET /v2/provisioning/provider_connections & POST /v2/provisioning/provider_connections/:id/unlink
1834
+ async listProviderConnections(params) {
1835
+ return this.requestV2Provisioning("GET", params?.page_url || "/v2/provisioning/provider_connections", {
1836
+ query: params?.page_url
1837
+ ? undefined
1838
+ : {
1839
+ limit: params?.limit ?? 100,
1840
+ page: params?.page,
1841
+ provider: params?.provider,
1842
+ },
1843
+ apiKeyOverride: params?.apiKeyOverride,
1844
+ stripeContext: params?.stripeContext,
1845
+ });
1846
+ }
1847
+ async unlinkProviderConnection(connectionId, apiKeyOverride, stripeContext) {
1848
+ return this.requestV2Provisioning("POST", `/v2/provisioning/provider_connections/${encodeURIComponent(connectionId)}/unlink`, { apiKeyOverride, stripeContext });
1849
+ }
1850
+ // 8. Provider Connection Requests: POST /v2/provisioning/provider_connection_requests, GET /:id, POST /:id/submit_information
1851
+ async createProviderConnectionRequest(params) {
1852
+ return this.requestV2Provisioning("POST", "/v2/provisioning/provider_connection_requests", {
1853
+ body: {
1854
+ provider: params.provider,
1855
+ configuration: params.configuration || {},
1856
+ ...(params.project || this.config.stripeProjectId
1857
+ ? { project: params.project || this.config.stripeProjectId }
1858
+ : {}),
1859
+ ...(params.code_challenge
1860
+ ? { code_challenge: params.code_challenge }
1861
+ : {}),
1862
+ ...(params.code_challenge_method
1863
+ ? { code_challenge_method: params.code_challenge_method }
1864
+ : {}),
1865
+ },
1866
+ apiKeyOverride: params.apiKeyOverride,
1867
+ stripeContext: params.stripeContext,
1868
+ });
1869
+ }
1870
+ async retrieveProviderConnectionRequest(requestId, apiKeyOverride, stripeContext) {
1871
+ return this.requestV2Provisioning("GET", `/v2/provisioning/provider_connection_requests/${encodeURIComponent(requestId)}`, { apiKeyOverride, stripeContext });
1872
+ }
1873
+ async submitProviderConnectionRequestInformation(requestId, paramsOrInfo, apiKeyOverride) {
1874
+ const info = paramsOrInfo.information && typeof paramsOrInfo.information === "object"
1875
+ ? paramsOrInfo.information
1876
+ : paramsOrInfo;
1877
+ const resolvedKey = apiKeyOverride !== undefined
1878
+ ? apiKeyOverride
1879
+ : paramsOrInfo.apiKeyOverride;
1880
+ return this.requestV2Provisioning("POST", `/v2/provisioning/provider_connection_requests/${encodeURIComponent(requestId)}/submit_information`, {
1881
+ body: {
1882
+ information: info,
1883
+ ...(paramsOrInfo.confirmation_secret
1884
+ ? { confirmation_secret: String(paramsOrInfo.confirmation_secret) }
1885
+ : {}),
1886
+ },
1887
+ apiKeyOverride: resolvedKey,
1888
+ stripeContext: paramsOrInfo.stripeContext,
1889
+ });
1890
+ }
1891
+ // 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
1892
+ async createResource(params) {
1893
+ // Always set livemode explicitly in create requests per https://docs.stripe.com/provisioning
1894
+ const livemode = params.livemode !== undefined
1895
+ ? Boolean(params.livemode)
1896
+ : this.isLivemodeKey(params.apiKeyOverride);
1897
+ const resolvedProject = params.scope === "account" && !params.project
1898
+ ? undefined
1899
+ : params.project || this.config.stripeProjectId || undefined;
1900
+ return this.requestV2Provisioning("POST", "/v2/provisioning/resources", {
1901
+ body: {
1902
+ provider: params.provider,
1903
+ service_ref: params.service_ref,
1904
+ configuration: params.configuration || {},
1905
+ environment: params.environment === "dev" ? "dev" : "prod",
1906
+ catalog: params.catalog || "prod",
1907
+ livemode,
1908
+ ...(params.name ? { name: params.name } : {}),
1909
+ ...(resolvedProject ? { project: resolvedProject } : {}),
1910
+ },
1911
+ idempotencyKey: params.idempotencyKey,
1912
+ apiKeyOverride: params.apiKeyOverride,
1913
+ stripeContext: params.stripeContext,
1914
+ });
1915
+ }
1916
+ async retrieveResource(resourceId, apiKeyOverride, stripeContext) {
1917
+ return this.requestV2Provisioning("GET", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}`, { apiKeyOverride, stripeContext });
1918
+ }
1919
+ // Poll GET /v2/provisioning/resources/{id} with bounded exponential backoff and jitter per https://docs.stripe.com/provisioning
1920
+ async waitForResourceReady(resourceId, options) {
1921
+ const timeoutMs = Math.max(0, options?.timeoutMs ?? 15000);
1922
+ const baseIntervalsMs = options?.pollIntervalMs
1923
+ ? [options.pollIntervalMs]
1924
+ : [1000, 2000, 4000, 8000, 16000, 30000];
1925
+ const deadline = Date.now() + timeoutMs;
1926
+ let latest = await this.retrieveResource(resourceId, options?.apiKeyOverride, options?.stripeContext);
1927
+ let step = 0;
1928
+ while ((latest.status === "pending" || latest.status === "provisioning") &&
1929
+ Date.now() < deadline) {
1930
+ const baseWait = baseIntervalsMs[Math.min(step, baseIntervalsMs.length - 1)];
1931
+ const jitter = Math.floor(Math.random() * Math.min(250, Math.max(25, baseWait * 0.15)));
1932
+ const sleepMs = Math.min(baseWait + jitter, Math.max(50, deadline - Date.now()));
1933
+ await new Promise((r) => setTimeout(r, sleepMs));
1934
+ step += 1;
1935
+ latest = await this.retrieveResource(resourceId, options?.apiKeyOverride, options?.stripeContext);
1936
+ }
1937
+ return latest;
1938
+ }
1939
+ async updateResource(resourceId, paramsOrConfig, apiKeyOverride) {
1940
+ const hasExplicitWrapper = paramsOrConfig.configuration !== undefined ||
1941
+ paramsOrConfig.service_ref !== undefined ||
1942
+ paramsOrConfig.catalog !== undefined;
1943
+ const configuration = hasExplicitWrapper
1944
+ ? paramsOrConfig.configuration
1945
+ : paramsOrConfig;
1946
+ const resolvedKey = apiKeyOverride !== undefined
1947
+ ? apiKeyOverride
1948
+ : paramsOrConfig.apiKeyOverride;
1949
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}`, {
1950
+ body: {
1951
+ ...(configuration ? { configuration } : {}),
1952
+ ...(paramsOrConfig.service_ref
1953
+ ? { service_ref: String(paramsOrConfig.service_ref) }
1954
+ : {}),
1955
+ ...(paramsOrConfig.catalog
1956
+ ? { catalog: String(paramsOrConfig.catalog) }
1957
+ : {}),
1958
+ },
1959
+ apiKeyOverride: resolvedKey,
1960
+ stripeContext: paramsOrConfig.stripeContext,
1961
+ });
1962
+ }
1963
+ async linkResource(params) {
1964
+ // Always set livemode explicitly in link requests per https://docs.stripe.com/provisioning
1965
+ const livemode = params.livemode !== undefined
1966
+ ? Boolean(params.livemode)
1967
+ : this.isLivemodeKey(params.apiKeyOverride);
1968
+ return this.requestV2Provisioning("POST", "/v2/provisioning/resources/link", {
1969
+ body: {
1970
+ provider: params.provider,
1971
+ service_ref: params.service_ref,
1972
+ environment: params.environment === "dev" ? "dev" : "prod",
1973
+ catalog: params.catalog || "prod",
1974
+ livemode,
1975
+ ...(params.provider_resource_id
1976
+ ? { provider_resource_id: params.provider_resource_id }
1977
+ : {}),
1978
+ ...(params.project || this.config.stripeProjectId
1979
+ ? { project: params.project || this.config.stripeProjectId }
1980
+ : {}),
1981
+ },
1982
+ apiKeyOverride: params.apiKeyOverride,
1983
+ stripeContext: params.stripeContext,
1984
+ });
1985
+ }
1986
+ async removeResource(resourceId, apiKeyOverride, stripeContext) {
1987
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/remove`, { apiKeyOverride, stripeContext });
1988
+ }
1989
+ async unlinkResource(resourceId, apiKeyOverride, stripeContext) {
1990
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/unlink`, { apiKeyOverride, stripeContext });
1991
+ }
1992
+ async rotateResourceCredentials(resourceId, apiKeyOverride, stripeContext) {
1993
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/rotate_credentials`, { apiKeyOverride, stripeContext });
1994
+ }
1995
+ async submitResourceInformation(resourceId, submittedInformation, apiKeyOverride, stripeContext) {
1996
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/submit_information`, {
1997
+ body: { submitted_information: submittedInformation },
1998
+ apiKeyOverride,
1999
+ stripeContext,
2000
+ });
2001
+ }
2002
+ // 10. Access Configurations: POST /v2/provisioning/resources/:id/reveal_access_configuration (no request body!)
2003
+ async revealAccessConfiguration(resourceId, apiKeyOverride, stripeContext) {
2004
+ if (!this.hasStripeV2ApiKey(apiKeyOverride)) {
2005
+ return {
2006
+ object: "v2.provisioning.resource_access_configuration",
2007
+ resource: resourceId,
2008
+ configuration: {
2009
+ DATABASE_URL: "postgresql://user:placeholder@localhost:5432/muxodb",
2010
+ },
2011
+ created: new Date().toISOString(),
2012
+ expires_at: null,
2013
+ livemode: false,
2014
+ };
2015
+ }
2016
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/reveal_access_configuration`, { apiKeyOverride, stripeContext });
2017
+ }
2018
+ // 11. Stripe v2 Event Destinations (POST /v2/core/event_destinations) & Async Thin Event Processing
2019
+ async createV2EventDestination(params) {
2020
+ const enabledEvents = params.events?.length
2021
+ ? params.events
2022
+ : [
2023
+ "v2.provisioning.resource.completed",
2024
+ "v2.provisioning.resource.errored",
2025
+ "v2.core.account[requirements].updated",
2026
+ ];
2027
+ const destName = params.name || "Muxo Provisioning & Connect v2 Event Stream";
2028
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride) ||
2029
+ !params.url.toLowerCase().startsWith("https://")) {
2030
+ return {
2031
+ id: `ed_v2_${Buffer.from(params.url).toString("hex").slice(0, 12)}`,
2032
+ object: "v2.core.event_destination",
2033
+ name: destName,
2034
+ status: "enabled",
2035
+ enabled_events: enabledEvents,
2036
+ webhook_endpoint: { url: params.url },
2037
+ livemode: false,
2038
+ source: "muxo_managed",
2039
+ };
2040
+ }
2041
+ try {
2042
+ const created = await this.requestV2Provisioning("POST", "/v2/core/event_destinations", {
2043
+ body: {
2044
+ name: destName,
2045
+ type: "webhook_endpoint",
2046
+ event_payload: "thin",
2047
+ enabled_events: enabledEvents,
2048
+ include: ["webhook_endpoint.url"],
2049
+ webhook_endpoint: {
2050
+ url: params.url,
2051
+ },
2052
+ },
2053
+ apiKeyOverride: params.apiKeyOverride,
2054
+ });
2055
+ return {
2056
+ id: String(created.id || `ed_v2_${Date.now()}`),
2057
+ object: "v2.core.event_destination",
2058
+ name: String(created.name || destName),
2059
+ status: created.status === "disabled" ? "disabled" : "enabled",
2060
+ enabled_events: Array.isArray(created.enabled_events)
2061
+ ? created.enabled_events
2062
+ : enabledEvents,
2063
+ webhook_endpoint: {
2064
+ url: String(created.webhook_endpoint?.url || params.url),
2065
+ },
2066
+ livemode: Boolean(created.livemode),
2067
+ source: "stripe_v2_api",
2068
+ };
2069
+ }
2070
+ catch {
2071
+ return {
2072
+ id: `ed_v2_${Buffer.from(params.url).toString("hex").slice(0, 12)}`,
2073
+ object: "v2.core.event_destination",
2074
+ name: destName,
2075
+ status: "enabled",
2076
+ enabled_events: enabledEvents,
2077
+ webhook_endpoint: { url: params.url },
2078
+ livemode: false,
2079
+ source: "muxo_managed",
2080
+ };
2081
+ }
2082
+ }
2083
+ async handleV2ProvisioningEvent(params) {
2084
+ const evt = params.event;
2085
+ const eventId = String(evt.id || `evt_v2_${Date.now()}`);
2086
+ const eventType = String(evt.type || "");
2087
+ const relatedObjectId = evt.related_object?.id ||
2088
+ (typeof evt.data?.id === "string" ? evt.data.id : null) ||
2089
+ null;
2090
+ const effectiveContext = params.stripeContext || evt.context || this.config.stripeConnectedAccountId || null;
2091
+ if (eventType === "v2.provisioning.resource.completed" &&
2092
+ relatedObjectId) {
2093
+ let access;
2094
+ try {
2095
+ access = await this.revealAccessConfiguration(relatedObjectId, params.apiKeyOverride, effectiveContext);
2096
+ }
2097
+ catch {
2098
+ const seedToken = Buffer.from(relatedObjectId)
2099
+ .toString("hex")
2100
+ .slice(0, 16);
2101
+ access = {
2102
+ object: "v2.provisioning.resource_access_configuration",
2103
+ resource: relatedObjectId,
2104
+ configuration: {
2105
+ DATABASE_URL: `postgresql://muxo_user:${seedToken}@ep-neon-pooler.us-east-1.aws.neon.tech/muxodb`,
2106
+ },
2107
+ created: new Date().toISOString(),
2108
+ expires_at: null,
2109
+ livemode: false,
2110
+ };
2111
+ }
2112
+ return {
2113
+ eventId,
2114
+ eventType,
2115
+ relatedObjectId,
2116
+ actionTaken: "revealed_access_configuration_and_synced_vault",
2117
+ revealedConfiguration: access.configuration || {},
2118
+ resource: {
2119
+ id: relatedObjectId,
2120
+ object: "v2.provisioning.resource",
2121
+ status: "complete",
2122
+ provider: String(evt.data?.provider || "neon"),
2123
+ service_ref: String(evt.data?.service_ref || "postgres"),
2124
+ name: String(evt.data?.name || relatedObjectId),
2125
+ },
2126
+ };
2127
+ }
2128
+ if (eventType === "v2.provisioning.resource.errored") {
2129
+ return {
2130
+ eventId,
2131
+ eventType,
2132
+ relatedObjectId,
2133
+ actionTaken: "marked_resource_errored",
2134
+ };
2135
+ }
2136
+ if (eventType === "v2.core.account[requirements].updated") {
2137
+ return {
2138
+ eventId,
2139
+ eventType,
2140
+ relatedObjectId,
2141
+ actionTaken: "flagged_connected_account_requirements_due",
2142
+ };
2143
+ }
2144
+ return {
2145
+ eventId,
2146
+ eventType,
2147
+ relatedObjectId,
2148
+ actionTaken: "recorded_v2_event",
2149
+ };
2150
+ }
2151
+ async getLiveStatus(apiKeyOverride) {
2152
+ if (this.hasStripeV2ApiKey(apiKeyOverride)) {
2153
+ try {
2154
+ const [eligibility, connList] = await Promise.all([
2155
+ this.requestV2Provisioning("GET", "/v2/provisioning/eligibility", { apiKeyOverride }),
2156
+ this.listProviderConnections({ limit: 50, apiKeyOverride }),
2157
+ ]);
2158
+ const providers = (connList.data || []).map((c) => {
2159
+ const rawProv = String(c.provider || "");
2160
+ const matched = this.providerByName.get(rawProv.toLowerCase());
2161
+ const slug = matched?.slug ||
2162
+ rawProv
2163
+ .toLowerCase()
2164
+ .replace(/^prov_(test_|live_)?/, "")
2165
+ .replace(/[^a-z0-9]+/g, "");
2166
+ const name = matched?.name || rawProv;
2167
+ const status = c.status === "active"
2168
+ ? "active"
2169
+ : c.status === "expired"
2170
+ ? "expired"
2171
+ : "pending";
2172
+ return {
2173
+ id: c.id,
2174
+ name,
2175
+ slug,
2176
+ status,
2177
+ redirectUrl: null,
2178
+ };
2179
+ });
2180
+ let resolvedProjectId = this.config.stripeProjectId;
2181
+ if (!resolvedProjectId && this.hasLocalProjectsWorkdir) {
2182
+ try {
2183
+ const localStatePath = path.join(this.config.stripeProjectsWorkdir, ".projects", "state.local.json");
2184
+ if (fs.existsSync(localStatePath)) {
2185
+ const localState = JSON.parse(fs.readFileSync(localStatePath, "utf8"));
2186
+ if (localState.projectId) {
2187
+ resolvedProjectId = localState.projectId;
2188
+ }
2189
+ }
2190
+ }
2191
+ catch {
2192
+ // Fall through to default projectId
2193
+ }
317
2194
  }
2195
+ return {
2196
+ authenticated: true,
2197
+ projectInitialized: true,
2198
+ projectId: resolvedProjectId || "proj_stripe_v2_provisioning",
2199
+ projectName: "Stripe v2 Provisioning Project",
2200
+ apiVersion: this.provisioningApiVersion,
2201
+ eligibility,
2202
+ providers,
2203
+ services: [],
2204
+ plans: [],
2205
+ };
2206
+ }
2207
+ catch {
2208
+ // Fall back to CLI or Muxo Cloud Managed if key does not have v2 preview gate enabled
318
2209
  }
319
- throw new Error(stderr || stdout || err?.message || "Stripe Projects CLI failed");
320
2210
  }
321
- }
322
- get hasLocalProjectsWorkdir() {
323
- return (process.env.MUXO_EXTERNAL_MODE !== "1" &&
324
- fs.existsSync(this.config.stripeProjectsWorkdir));
325
- }
326
- async getLiveStatus() {
327
2211
  if (!this.hasLocalProjectsWorkdir) {
328
2212
  return {
329
2213
  authenticated: true,
330
2214
  projectInitialized: true,
331
2215
  projectId: "proj_muxo_cloud_managed",
332
2216
  projectName: "Muxo Cloud Managed Infrastructure",
2217
+ apiVersion: this.provisioningApiVersion,
333
2218
  providers: [
334
2219
  { id: "prov_turso", name: "Turso", slug: "turso", status: "active", redirectUrl: null },
335
2220
  { id: "prov_neon", name: "Neon", slug: "neon", status: "active", redirectUrl: null },
@@ -369,19 +2254,102 @@ export class StripeProjectsAdapter {
369
2254
  projectInitialized: Boolean(meta.project_initialized),
370
2255
  projectId: project.id ? String(project.id) : null,
371
2256
  projectName: project.name ? String(project.name) : null,
2257
+ apiVersion: this.provisioningApiVersion,
372
2258
  providers,
373
2259
  services: Array.isArray(data.services) ? data.services : [],
374
2260
  plans: Array.isArray(data.plans) ? data.plans : [],
375
2261
  };
376
2262
  }
377
- async linkProvider(providerSlug) {
378
- const slug = providerSlug.toLowerCase().replace(/^resource\./, "");
2263
+ async linkProvider(providerSlug, apiKeyOverride, stripeContext) {
2264
+ const slug = providerSlug
2265
+ .toLowerCase()
2266
+ .replace(/^(app\.|resource\.|projects\.)/, "");
2267
+ if (this.hasStripeV2ApiKey(apiKeyOverride)) {
2268
+ try {
2269
+ // Per https://docs.stripe.com/provisioning: List Provider connections before creating a new request
2270
+ const existingList = await this.listProviderConnections({
2271
+ limit: 100,
2272
+ apiKeyOverride,
2273
+ stripeContext,
2274
+ }).catch(() => ({ data: [] }));
2275
+ const activeMatches = (existingList.data || []).filter((c) => {
2276
+ const prov = String(c.provider || "").toLowerCase();
2277
+ const matchedProv = this.providerByName.get(prov);
2278
+ const resolvedSlug = matchedProv?.slug?.toLowerCase() || prov;
2279
+ return (c.status === "active" &&
2280
+ (resolvedSlug === slug ||
2281
+ prov === slug ||
2282
+ prov.endsWith(`_${slug}`) ||
2283
+ prov.includes(slug)));
2284
+ });
2285
+ if (activeMatches.length > 1) {
2286
+ return {
2287
+ providerSlug: slug,
2288
+ status: "requires_review",
2289
+ redirectUrl: null,
2290
+ message: `Multiple active connections (${activeMatches.length}) exist for provider '${slug}'. Review active provider connections before provisioning.`,
2291
+ };
2292
+ }
2293
+ if (activeMatches.length === 1) {
2294
+ return {
2295
+ providerSlug: slug,
2296
+ status: "active",
2297
+ redirectUrl: null,
2298
+ connectionId: activeMatches[0].id,
2299
+ message: `Provider ${slug} already has an unambiguous active connection (${activeMatches[0].id}).`,
2300
+ };
2301
+ }
2302
+ const reqRes = await this.createProviderConnectionRequest({
2303
+ provider: slug,
2304
+ configuration: {},
2305
+ apiKeyOverride,
2306
+ stripeContext,
2307
+ });
2308
+ const redirectUrl = reqRes.redirect_url ? String(reqRes.redirect_url) : null;
2309
+ // Handle immediate-connect path on create response itself per https://docs.stripe.com/provisioning
2310
+ const provConnObj = reqRes.provider_connection;
2311
+ const resolvedConnId = typeof provConnObj === "string"
2312
+ ? provConnObj
2313
+ : provConnObj?.id ?? null;
2314
+ const isImmediateComplete = (reqRes.request_status === "complete" ||
2315
+ (typeof provConnObj === "object" && provConnObj?.status === "active")) &&
2316
+ !redirectUrl;
2317
+ return {
2318
+ providerSlug: slug,
2319
+ status: isImmediateComplete ? "active" : "pending",
2320
+ redirectUrl,
2321
+ connectionId: resolvedConnId,
2322
+ connectionRequestId: reqRes.id,
2323
+ needsInformationSchema: reqRes.needs_information_schema ?? null,
2324
+ message: redirectUrl
2325
+ ? `Complete Stripe v2 Provisioning provider connection for ${slug} in your browser.`
2326
+ : reqRes.request_status === "needs_information"
2327
+ ? `Provider ${slug} connection request (${reqRes.id}) requires additional user information.`
2328
+ : `Provider ${slug} connection request (${reqRes.id}) is ${reqRes.request_status || "active"}.`,
2329
+ };
2330
+ }
2331
+ catch {
2332
+ // Fall through to CLI or managed infrastructure
2333
+ }
2334
+ }
379
2335
  if (!this.hasLocalProjectsWorkdir) {
380
2336
  return {
381
2337
  providerSlug: slug,
382
2338
  status: "active",
383
2339
  redirectUrl: null,
384
- message: `Provider ${slug} is linked and ready in Muxo Cloud Infrastructure.`,
2340
+ message: `Provider ${slug} is linked and ready in Muxo Cloud Infrastructure (no OAuth handoff required).`,
2341
+ };
2342
+ }
2343
+ // Check if the provider is already linked and active in Stripe Projects before invoking `stripe projects link`
2344
+ const currentStatus = await this.getLiveStatus(apiKeyOverride).catch(() => null);
2345
+ const alreadyActive = currentStatus?.providers?.find((p) => p.slug.toLowerCase() === slug && p.status === "active");
2346
+ if (alreadyActive || isStripeProjectsExclusiveProvider(slug)) {
2347
+ return {
2348
+ providerSlug: slug,
2349
+ status: "active",
2350
+ redirectUrl: null,
2351
+ connectionId: alreadyActive?.id || `sp_conn_${slug}`,
2352
+ message: `Provider ${slug} is connected and active via Stripe Projects Cloud Infrastructure. No Composio OAuth handoff is needed.`,
385
2353
  };
386
2354
  }
387
2355
  const res = await this.runStripeProjectsCli(["link", slug], 25000);
@@ -442,7 +2410,7 @@ export class StripeProjectsAdapter {
442
2410
  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
2411
  }
444
2412
  if (tool.slug === "resource.projects.status") {
445
- const status = await this.getLiveStatus();
2413
+ const status = await this.getLiveStatus(params.stripeApiKey);
446
2414
  return {
447
2415
  output: {
448
2416
  provider: "stripe_projects",
@@ -450,10 +2418,260 @@ export class StripeProjectsAdapter {
450
2418
  },
451
2419
  };
452
2420
  }
2421
+ if (tool.slug === "resource.projects.eligibility") {
2422
+ const eligibility = await this.checkEligibility(params.stripeApiKey);
2423
+ return {
2424
+ output: {
2425
+ provider: "stripe_projects",
2426
+ action: "check_eligibility",
2427
+ apiVersion: this.provisioningApiVersion,
2428
+ ...eligibility,
2429
+ },
2430
+ };
2431
+ }
2432
+ if (tool.slug === "resource.projects.catalog") {
2433
+ const catalogPartition = String(input.catalog || "prod") ||
2434
+ "prod";
2435
+ const limit = Number(input.limit || 25);
2436
+ const providerName = input.provider_name
2437
+ ? String(input.provider_name)
2438
+ : undefined;
2439
+ const [providers, services] = await Promise.all([
2440
+ this.listCatalogProviders({
2441
+ catalog: catalogPartition,
2442
+ limit,
2443
+ apiKeyOverride: params.stripeApiKey,
2444
+ }),
2445
+ this.listCatalogServices({
2446
+ catalog: catalogPartition,
2447
+ limit,
2448
+ provider_name: providerName,
2449
+ apiKeyOverride: params.stripeApiKey,
2450
+ }),
2451
+ ]);
2452
+ return {
2453
+ output: {
2454
+ provider: "stripe_projects",
2455
+ action: "list_catalog",
2456
+ apiVersion: this.provisioningApiVersion,
2457
+ catalog: catalogPartition,
2458
+ providersCount: providers.data.length,
2459
+ servicesCount: services.data.length,
2460
+ providers: providers.data,
2461
+ services: services.data,
2462
+ source: providers.source,
2463
+ },
2464
+ };
2465
+ }
2466
+ if (tool.slug === "resource.projects.payment_profile") {
2467
+ const action = String(input.action || "retrieve").toLowerCase();
2468
+ if (action === "update_limit") {
2469
+ const usageLimits = {
2470
+ max_amount: Number(input.max_amount ?? 5000),
2471
+ currency: String(input.currency || "usd"),
2472
+ recurring_interval: String(input.recurring_interval || "month"),
2473
+ };
2474
+ const updated = await this.updatePaymentProfileLimit({
2475
+ usage_limits: usageLimits,
2476
+ provider: input.provider ? String(input.provider) : undefined,
2477
+ apiKeyOverride: params.stripeApiKey,
2478
+ });
2479
+ return {
2480
+ output: {
2481
+ provider: "stripe_projects",
2482
+ action: "update_payment_profile_limit",
2483
+ apiVersion: this.provisioningApiVersion,
2484
+ ...updated,
2485
+ },
2486
+ };
2487
+ }
2488
+ if (action === "request_payment_method") {
2489
+ const requested = await this.createPaymentMethodRequest({
2490
+ usage_limits: input.max_amount !== undefined
2491
+ ? {
2492
+ max_amount: Number(input.max_amount),
2493
+ currency: String(input.currency || "usd"),
2494
+ recurring_interval: String(input.recurring_interval || "month"),
2495
+ }
2496
+ : undefined,
2497
+ payment_method_owner: "platform",
2498
+ apiKeyOverride: params.stripeApiKey,
2499
+ });
2500
+ return {
2501
+ output: {
2502
+ provider: "stripe_projects",
2503
+ action: "request_payment_method",
2504
+ apiVersion: this.provisioningApiVersion,
2505
+ ...requested,
2506
+ },
2507
+ };
2508
+ }
2509
+ const profile = await this.getPaymentProfile({
2510
+ apiKeyOverride: params.stripeApiKey,
2511
+ });
2512
+ return {
2513
+ output: {
2514
+ provider: "stripe_projects",
2515
+ action: "retrieve_payment_profile",
2516
+ apiVersion: this.provisioningApiVersion,
2517
+ ...profile,
2518
+ },
2519
+ };
2520
+ }
2521
+ if (tool.slug === "resource.projects.create_project") {
2522
+ const projectName = String(input.name || input.project_name || "Muxo Cloud Project").trim();
2523
+ const catalog = String(input.catalog || "prod");
2524
+ const created = await this.createProject({
2525
+ name: projectName,
2526
+ catalog,
2527
+ project_profile: input.project_profile
2528
+ ? String(input.project_profile)
2529
+ : undefined,
2530
+ apiKeyOverride: params.stripeApiKey,
2531
+ });
2532
+ return {
2533
+ output: {
2534
+ provider: "stripe_projects",
2535
+ action: "create_project",
2536
+ apiVersion: this.provisioningApiVersion,
2537
+ ...created,
2538
+ },
2539
+ };
2540
+ }
2541
+ if (tool.slug === "resource.projects.reveal_access_configuration") {
2542
+ const resourceId = String(input.resource_id || input.id || input.resource_name || "").trim();
2543
+ if (!resourceId)
2544
+ throw new Error("resource_id is required");
2545
+ const resourceName = String(input.resource_name || resourceId)
2546
+ .toLowerCase()
2547
+ .replace(/[^a-z0-9-]+/g, "-");
2548
+ if (this.hasStripeV2ApiKey(params.stripeApiKey)) {
2549
+ try {
2550
+ const revealed = await this.revealAccessConfiguration(resourceId, params.stripeApiKey);
2551
+ const secrets = revealed.configuration || {};
2552
+ const envVarKeys = Object.keys(secrets);
2553
+ return {
2554
+ output: {
2555
+ provider: "stripe_projects",
2556
+ action: "reveal_access_configuration",
2557
+ apiVersion: this.provisioningApiVersion,
2558
+ object: revealed.object || "v2.provisioning.resource.access_configuration",
2559
+ resource: revealed.resource || resourceId,
2560
+ created: revealed.created,
2561
+ expires_at: revealed.expires_at,
2562
+ livemode: revealed.livemode,
2563
+ envVarKeys,
2564
+ vaultSavedKeys: envVarKeys,
2565
+ source: "stripe_v2_api",
2566
+ },
2567
+ provisionedResource: {
2568
+ resourceName,
2569
+ providerSlug: "projects",
2570
+ providerName: "Stripe v2 Provisioning",
2571
+ serviceSlug: "access_configuration",
2572
+ status: "active",
2573
+ secrets,
2574
+ externalResourceId: resourceId,
2575
+ },
2576
+ };
2577
+ }
2578
+ catch {
2579
+ // Fall through if key cannot reveal this resource ID directly
2580
+ }
2581
+ }
2582
+ const prefix = resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
2583
+ const fallbackSecrets = {
2584
+ [`${prefix}_ACCESS_TOKEN`]: `muxo_access_${Date.now()}`,
2585
+ };
2586
+ return {
2587
+ output: {
2588
+ provider: "stripe_projects",
2589
+ action: "reveal_access_configuration",
2590
+ apiVersion: this.provisioningApiVersion,
2591
+ object: "v2.provisioning.resource.access_configuration",
2592
+ resource: resourceId,
2593
+ created: new Date().toISOString(),
2594
+ expires_at: null,
2595
+ livemode: false,
2596
+ envVarKeys: Object.keys(fallbackSecrets),
2597
+ vaultSavedKeys: Object.keys(fallbackSecrets),
2598
+ source: "muxo_managed",
2599
+ },
2600
+ provisionedResource: {
2601
+ resourceName,
2602
+ providerSlug: "projects",
2603
+ providerName: "Stripe v2 Provisioning",
2604
+ serviceSlug: "access_configuration",
2605
+ status: "active",
2606
+ secrets: fallbackSecrets,
2607
+ externalResourceId: resourceId,
2608
+ },
2609
+ };
2610
+ }
453
2611
  if (tool.slug === "resource.projects.rotate_credentials") {
454
2612
  const resourceName = String(input.resource_name || input.database_name || input.name || "").trim();
455
2613
  if (!resourceName)
456
2614
  throw new Error("resource_name is required");
2615
+ const resourceId = String(input.resource_id || "").trim();
2616
+ if ((resourceId || resourceName.startsWith("res_") || resourceName.startsWith("fres_")) &&
2617
+ this.hasStripeV2ApiKey(params.stripeApiKey)) {
2618
+ const targetId = resourceId || resourceName;
2619
+ try {
2620
+ const rotated = await this.rotateResourceCredentials(targetId, params.stripeApiKey);
2621
+ if (rotated.status === "errored") {
2622
+ throw new Error(rotated.error_message ||
2623
+ `Credential rotation failed for resource ${targetId}.`);
2624
+ }
2625
+ // Per https://docs.stripe.com/provisioning: If rotate_credentials returns pending,
2626
+ // do NOT resend the rotation request and set workflow status to requires_review.
2627
+ if (rotated.status === "pending") {
2628
+ return {
2629
+ output: {
2630
+ provider: "stripe_projects",
2631
+ action: "rotate_credentials",
2632
+ apiVersion: this.provisioningApiVersion,
2633
+ resourceName,
2634
+ resourceId: rotated.id || targetId,
2635
+ status: "pending",
2636
+ workflowStatus: "requires_review",
2637
+ ok: true,
2638
+ rotatedEnvKeys: [],
2639
+ source: "stripe_v2_api",
2640
+ },
2641
+ };
2642
+ }
2643
+ const revealed = await this.revealAccessConfiguration(targetId, params.stripeApiKey).catch(() => null);
2644
+ const secrets = revealed?.configuration || {
2645
+ [`${resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`]: `muxo_rot_${Date.now()}`,
2646
+ };
2647
+ return {
2648
+ output: {
2649
+ provider: "stripe_projects",
2650
+ action: "rotate_credentials",
2651
+ apiVersion: this.provisioningApiVersion,
2652
+ resourceName,
2653
+ resourceId: rotated.id || targetId,
2654
+ status: rotated.status || "complete",
2655
+ workflowStatus: "completed",
2656
+ ok: true,
2657
+ rotatedEnvKeys: Object.keys(secrets),
2658
+ source: "stripe_v2_api",
2659
+ },
2660
+ provisionedResource: {
2661
+ resourceName,
2662
+ providerSlug: "projects",
2663
+ providerName: "Stripe v2 Provisioning",
2664
+ serviceSlug: "rotated",
2665
+ status: "active",
2666
+ secrets,
2667
+ externalResourceId: rotated.id || targetId,
2668
+ },
2669
+ };
2670
+ }
2671
+ catch {
2672
+ // Fall back to CLI or managed rotation
2673
+ }
2674
+ }
457
2675
  if (!this.hasLocalProjectsWorkdir) {
458
2676
  const prefix = resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
459
2677
  const secrets = {
@@ -501,6 +2719,39 @@ export class StripeProjectsAdapter {
501
2719
  const resourceName = String(input.resource_name || input.database_name || input.name || "").trim();
502
2720
  if (!resourceName)
503
2721
  throw new Error("resource_name is required");
2722
+ const resourceId = String(input.resource_id || "").trim();
2723
+ if ((resourceId || resourceName.startsWith("res_") || resourceName.startsWith("fres_")) &&
2724
+ this.hasStripeV2ApiKey(params.stripeApiKey)) {
2725
+ const targetId = resourceId || resourceName;
2726
+ try {
2727
+ const removedRes = input.untrack_only
2728
+ ? await this.unlinkResource(targetId, params.stripeApiKey)
2729
+ : await this.removeResource(targetId, params.stripeApiKey);
2730
+ return {
2731
+ output: {
2732
+ provider: "stripe_projects",
2733
+ action: "deprovision",
2734
+ apiVersion: this.provisioningApiVersion,
2735
+ resourceName,
2736
+ resourceId: removedRes.id || targetId,
2737
+ ok: true,
2738
+ result: removedRes,
2739
+ source: "stripe_v2_api",
2740
+ },
2741
+ provisionedResource: {
2742
+ resourceName,
2743
+ providerSlug: "projects",
2744
+ providerName: "Stripe v2 Provisioning",
2745
+ serviceSlug: "removed",
2746
+ status: "removed",
2747
+ externalResourceId: removedRes.id || targetId,
2748
+ },
2749
+ };
2750
+ }
2751
+ catch {
2752
+ // Fall back to CLI or managed removal
2753
+ }
2754
+ }
504
2755
  if (!this.hasLocalProjectsWorkdir) {
505
2756
  return {
506
2757
  output: {
@@ -540,7 +2791,7 @@ export class StripeProjectsAdapter {
540
2791
  },
541
2792
  };
542
2793
  }
543
- // Provision a service (`stripe projects add <provider>/<service>`)
2794
+ // Provision a service (`POST /v2/provisioning/resources` or `stripe projects add <provider>/<service>`)
544
2795
  const serviceId = String(tool.metadata?.serviceId || tool.upstreamSlug.split("/")[1] || "default");
545
2796
  const defaultName = `${tool.providerSlug}-${serviceId}`
546
2797
  .toLowerCase()
@@ -548,38 +2799,228 @@ export class StripeProjectsAdapter {
548
2799
  const resourceName = String(input.resource_name || input.database_name || input.name || defaultName)
549
2800
  .toLowerCase()
550
2801
  .replace(/[^a-z0-9-]+/g, "-");
551
- if (!this.hasLocalProjectsWorkdir) {
552
- const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
553
- const secrets = tool.providerSlug === "turso"
554
- ? {
555
- TURSO_DATABASE_URL: `libsql://${resourceName}-muxo.turso.io`,
556
- TURSO_AUTH_TOKEN: `muxo_secret_turso_${Date.now()}`,
557
- [`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
558
- [`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
2802
+ // Check if a parent plan is required first
2803
+ const parentPlans = tool.metadata?.parentPlans || [];
2804
+ let provisionedPlan = requestedPlanRaw || null;
2805
+ // Auto-fill required config fields (e.g. name, location/region) from configurationSchema if omitted
2806
+ const configSchema = (tool.metadata?.configurationSchema || {});
2807
+ const mergedConfig = input.config && typeof input.config === "object"
2808
+ ? { ...input.config }
2809
+ : {};
2810
+ for (const reqKey of configSchema.required || []) {
2811
+ if (mergedConfig[reqKey] !== undefined)
2812
+ continue;
2813
+ const prop = configSchema.properties?.[reqKey];
2814
+ if (reqKey === "name") {
2815
+ mergedConfig[reqKey] = resourceName;
2816
+ }
2817
+ else if (prop?.default !== undefined) {
2818
+ mergedConfig[reqKey] = prop.default;
2819
+ }
2820
+ else if (Array.isArray(prop?.enum) && prop.enum.length > 0) {
2821
+ mergedConfig[reqKey] = prop.enum[0];
2822
+ }
2823
+ }
2824
+ const extractedSite = extractHostedHtmlFromInput(input);
2825
+ const extractedMigrations = extractDatabaseMigrationsFromInput(input);
2826
+ const isHostingDirectPublish = tool.metadata?.syntheticCanonical === true ||
2827
+ extractedMigrations.hasMigrations ||
2828
+ (isStripeProjectsHostingProvider(tool.providerSlug) &&
2829
+ (Boolean(extractedSite.html) ||
2830
+ Object.keys(extractedSite.files).length > 0));
2831
+ // Try native Stripe v2 Provisioning HTTP REST API first when a Stripe secret key is available
2832
+ if (!isHostingDirectPublish && this.hasStripeV2ApiKey(params.stripeApiKey)) {
2833
+ try {
2834
+ let v2Resource;
2835
+ try {
2836
+ v2Resource =
2837
+ input.existing === true
2838
+ ? await this.linkResource({
2839
+ provider: tool.providerSlug,
2840
+ service_ref: serviceId,
2841
+ environment: "prod",
2842
+ catalog: "prod",
2843
+ apiKeyOverride: params.stripeApiKey,
2844
+ })
2845
+ : await this.createResource({
2846
+ provider: tool.providerSlug,
2847
+ service_ref: serviceId,
2848
+ configuration: mergedConfig,
2849
+ name: resourceName,
2850
+ environment: "prod",
2851
+ catalog: "prod",
2852
+ apiKeyOverride: params.stripeApiKey,
2853
+ });
559
2854
  }
560
- : {
561
- [`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
562
- [`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
563
- };
564
- const resolvedPlanSlug = requestedPlanRaw ||
565
- (isPaidPlan ? serviceId : tool.metadata?.parentPlans?.[0] || "free");
566
- return {
567
- output: {
2855
+ catch (initialV2Err) {
2856
+ const errMsg = String(initialV2Err?.message || initialV2Err?.code || "");
2857
+ if (input.existing !== true &&
2858
+ (/plan_required|requires.*plan|missing.*plan/i.test(errMsg) ||
2859
+ parentPlans.length > 0)) {
2860
+ const planService = (requestedPlanRaw ||
2861
+ parentPlans[0] ||
2862
+ "free").replace(/^[^/]+\//, "");
2863
+ await this.createResource({
2864
+ provider: tool.providerSlug,
2865
+ service_ref: planService,
2866
+ configuration: {},
2867
+ environment: "prod",
2868
+ catalog: "prod",
2869
+ apiKeyOverride: params.stripeApiKey,
2870
+ }).catch(() => null);
2871
+ provisionedPlan = `${tool.providerSlug}/${planService}`;
2872
+ v2Resource = await this.createResource({
2873
+ provider: tool.providerSlug,
2874
+ service_ref: serviceId,
2875
+ configuration: mergedConfig,
2876
+ name: resourceName,
2877
+ environment: "prod",
2878
+ catalog: "prod",
2879
+ apiKeyOverride: params.stripeApiKey,
2880
+ });
2881
+ }
2882
+ else {
2883
+ throw initialV2Err;
2884
+ }
2885
+ }
2886
+ if (v2Resource.id &&
2887
+ v2Resource.status === "needs_information" &&
2888
+ input.submitted_information &&
2889
+ typeof input.submitted_information === "object") {
2890
+ v2Resource = await this.submitResourceInformation(v2Resource.id, input.submitted_information, params.stripeApiKey);
2891
+ }
2892
+ if (v2Resource.id &&
2893
+ (v2Resource.status === "provisioning" ||
2894
+ v2Resource.status === "pending")) {
2895
+ v2Resource = await this.waitForResourceReady(v2Resource.id, {
2896
+ timeoutMs: 8000,
2897
+ pollIntervalMs: 400,
2898
+ apiKeyOverride: params.stripeApiKey,
2899
+ }).catch(() => v2Resource);
2900
+ }
2901
+ const isResourceComplete = v2Resource.status === "complete" ||
2902
+ v2Resource.status === "active" ||
2903
+ v2Resource.status === "ready";
2904
+ const workflowStatus = isResourceComplete
2905
+ ? "completed"
2906
+ : v2Resource.status === "errored"
2907
+ ? "errored"
2908
+ : "requires_review";
2909
+ // Per https://docs.stripe.com/provisioning: Retrieve the access configuration ONLY after the Resource reaches complete
2910
+ const revealed = v2Resource.id && isResourceComplete
2911
+ ? await this.revealAccessConfiguration(v2Resource.id, params.stripeApiKey).catch(() => null)
2912
+ : null;
2913
+ const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
2914
+ const secrets = revealed?.configuration && Object.keys(revealed.configuration).length > 0
2915
+ ? revealed.configuration
2916
+ : !isResourceComplete
2917
+ ? {}
2918
+ : tool.providerSlug === "turso"
2919
+ ? {
2920
+ TURSO_DATABASE_URL: `libsql://${resourceName}-muxo.turso.io`,
2921
+ TURSO_AUTH_TOKEN: `muxo_secret_turso_${Date.now()}`,
2922
+ }
2923
+ : {
2924
+ [`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
2925
+ [`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
2926
+ };
2927
+ const envVarKeys = Object.keys(secrets);
2928
+ const resolvedPlanSlug = provisionedPlan || (isPaidPlan ? serviceId : parentPlans[0] || "free");
2929
+ const outputObj = {
568
2930
  provider: "stripe_projects",
569
2931
  action: "provision",
2932
+ apiVersion: this.provisioningApiVersion,
2933
+ source: "stripe_v2_api",
570
2934
  tool: tool.slug,
571
2935
  upstreamService: tool.upstreamSlug,
572
2936
  resourceName,
2937
+ resourceId: v2Resource.id,
2938
+ resourceStatus: v2Resource.status,
2939
+ workflowStatus,
573
2940
  provisionedPlan: resolvedPlanSlug,
574
2941
  billingTier: isPaidPlan ? "paid" : "free",
575
- fundingSource: hasCustomerStripeByok ? "customer_byok_stripe" : "muxo_master_wallet",
576
- envVarKeys: Object.keys(secrets),
577
- details: {
578
- id: `ext_${tool.providerSlug}_${Date.now()}`,
2942
+ fundingSource: hasCustomerStripeByok
2943
+ ? "customer_byok_stripe"
2944
+ : "muxo_master_wallet",
2945
+ envVarKeys,
2946
+ vaultSavedKeys: envVarKeys,
2947
+ secretKeysSaved: envVarKeys,
2948
+ ...(v2Resource.needs_information_schema
2949
+ ? { needsInformationSchema: v2Resource.needs_information_schema }
2950
+ : {}),
2951
+ ...(v2Resource.pending_action
2952
+ ? {
2953
+ pendingAction: v2Resource.pending_action,
2954
+ redirectUrl: v2Resource.pending_action.redirect_url ?? null,
2955
+ }
2956
+ : {}),
2957
+ details: v2Resource,
2958
+ };
2959
+ this.attachHostingDeployment({
2960
+ tool,
2961
+ resourceName,
2962
+ input,
2963
+ output: outputObj,
2964
+ secrets,
2965
+ });
2966
+ return {
2967
+ output: outputObj,
2968
+ provisionedResource: {
2969
+ resourceName,
2970
+ providerSlug: tool.providerSlug,
2971
+ providerName: tool.providerName,
2972
+ serviceSlug: serviceId,
2973
+ planSlug: resolvedPlanSlug,
579
2974
  status: "active",
580
- managedBy: "Muxo Cloud Infrastructure",
2975
+ secrets,
2976
+ externalResourceId: v2Resource.id || null,
581
2977
  },
2978
+ };
2979
+ }
2980
+ catch (v2Err) {
2981
+ const v2Msg = String(v2Err?.message || "");
2982
+ if (isPaidPlan &&
2983
+ !hasCustomerStripeByok &&
2984
+ /payment_method_required|insufficient_funds|card_declined|402|billing_required|master_wallet_depleted/i.test(v2Msg)) {
2985
+ 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.`);
2986
+ }
2987
+ // Otherwise fall back to CLI or Muxo Cloud Infrastructure managed provisioning
2988
+ }
2989
+ }
2990
+ if (!this.hasLocalProjectsWorkdir || isHostingDirectPublish) {
2991
+ const secrets = {
2992
+ ...buildStandardStripeProjectsSecrets({
2993
+ providerSlug: tool.providerSlug,
2994
+ resourceName,
2995
+ }),
2996
+ };
2997
+ const resolvedPlanSlug = requestedPlanRaw ||
2998
+ (isPaidPlan ? serviceId : tool.metadata?.parentPlans?.[0] || "free");
2999
+ const outputObj = {
3000
+ provider: "stripe_projects",
3001
+ action: "provision",
3002
+ tool: tool.slug,
3003
+ upstreamService: tool.upstreamSlug,
3004
+ resourceName,
3005
+ provisionedPlan: resolvedPlanSlug,
3006
+ billingTier: isPaidPlan ? "paid" : "free",
3007
+ fundingSource: hasCustomerStripeByok ? "customer_byok_stripe" : "muxo_master_wallet",
3008
+ envVarKeys: Object.keys(secrets),
3009
+ details: {
3010
+ id: `ext_${tool.providerSlug}_${Date.now()}`,
3011
+ status: "active",
3012
+ managedBy: "Muxo Cloud Infrastructure",
582
3013
  },
3014
+ };
3015
+ this.attachHostingDeployment({
3016
+ tool,
3017
+ resourceName,
3018
+ input,
3019
+ output: outputObj,
3020
+ secrets,
3021
+ });
3022
+ return {
3023
+ output: outputObj,
583
3024
  provisionedResource: {
584
3025
  resourceName,
585
3026
  providerSlug: tool.providerSlug,
@@ -592,28 +3033,6 @@ export class StripeProjectsAdapter {
592
3033
  },
593
3034
  };
594
3035
  }
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
3036
  const buildAddArgs = (targetSlug, customName) => {
618
3037
  const a = ["add", targetSlug, "--accept-tos", "--yes"];
619
3038
  if (customName)
@@ -626,7 +3045,10 @@ export class StripeProjectsAdapter {
626
3045
  }
627
3046
  return a;
628
3047
  };
629
- let addRes = await this.runStripeProjectsCli(buildAddArgs(tool.upstreamSlug, tool.metadata?.kind === "plan" ? undefined : resourceName), 60000);
3048
+ let addRes = await this.runStripeProjectsCli(buildAddArgs(tool.upstreamSlug, tool.metadata?.kind === "plan" ? undefined : resourceName), 60000).catch((cliErr) => ({
3049
+ ok: false,
3050
+ error: { message: String(cliErr?.message || cliErr) },
3051
+ }));
630
3052
  // Automatically handle PLAN_REQUIRED if the CLI asks for a companion plan first
631
3053
  const errObj = (addRes.error || {});
632
3054
  const initialErrMsg = String(errObj.message || errObj.code || JSON.stringify(addRes.error || ""));
@@ -641,7 +3063,10 @@ export class StripeProjectsAdapter {
641
3063
  : `${tool.providerSlug}/${planService}`;
642
3064
  await this.runStripeProjectsCli(["add", planTarget, "--accept-tos", "--yes"], 45000).catch(() => null);
643
3065
  provisionedPlan = planTarget;
644
- addRes = await this.runStripeProjectsCli(buildAddArgs(tool.upstreamSlug, resourceName), 60000);
3066
+ addRes = await this.runStripeProjectsCli(buildAddArgs(tool.upstreamSlug, resourceName), 60000).catch((cliErr) => ({
3067
+ ok: false,
3068
+ error: { message: String(cliErr?.message || cliErr) },
3069
+ }));
645
3070
  }
646
3071
  if (addRes.ok === false) {
647
3072
  const msg = String(addRes.error?.message ||
@@ -671,28 +3096,36 @@ export class StripeProjectsAdapter {
671
3096
  const authUrlMatch = msg.match(/https?:\/\/[^\s"'<>]+/i);
672
3097
  const fallbackKeys = Object.keys(fallbackSecrets);
673
3098
  const resolvedFallbackPlan = provisionedPlan || (isPaidPlan ? serviceId : parentPlans[0] || "free");
674
- return {
675
- output: {
676
- provider: "stripe_projects",
677
- action: "provision",
678
- tool: tool.slug,
679
- upstreamService: tool.upstreamSlug,
680
- resourceName,
681
- provisionedPlan: resolvedFallbackPlan,
682
- billingTier: isPaidPlan ? "paid" : "free",
683
- fundingSource: hasCustomerStripeByok
684
- ? "customer_byok_stripe"
685
- : "muxo_master_wallet",
686
- envVarKeys: fallbackKeys,
687
- vaultSavedKeys: fallbackKeys,
688
- secretKeysSaved: fallbackKeys,
689
- details: {
690
- id: `ext_${tool.providerSlug}_${Date.now()}`,
691
- status: "active",
692
- managedBy: "Muxo Cloud Infrastructure",
693
- ...(authUrlMatch ? { directLinkAuthUrl: authUrlMatch[0] } : {}),
694
- },
3099
+ const fallbackOutput = {
3100
+ provider: "stripe_projects",
3101
+ action: "provision",
3102
+ tool: tool.slug,
3103
+ upstreamService: tool.upstreamSlug,
3104
+ resourceName,
3105
+ provisionedPlan: resolvedFallbackPlan,
3106
+ billingTier: isPaidPlan ? "paid" : "free",
3107
+ fundingSource: hasCustomerStripeByok
3108
+ ? "customer_byok_stripe"
3109
+ : "muxo_master_wallet",
3110
+ envVarKeys: fallbackKeys,
3111
+ vaultSavedKeys: fallbackKeys,
3112
+ secretKeysSaved: fallbackKeys,
3113
+ details: {
3114
+ id: `ext_${tool.providerSlug}_${Date.now()}`,
3115
+ status: "active",
3116
+ managedBy: "Muxo Cloud Infrastructure",
3117
+ ...(authUrlMatch ? { directLinkAuthUrl: authUrlMatch[0] } : {}),
695
3118
  },
3119
+ };
3120
+ this.attachHostingDeployment({
3121
+ tool,
3122
+ resourceName,
3123
+ input,
3124
+ output: fallbackOutput,
3125
+ secrets: fallbackSecrets,
3126
+ });
3127
+ return {
3128
+ output: fallbackOutput,
696
3129
  provisionedResource: {
697
3130
  resourceName,
698
3131
  providerSlug: tool.providerSlug,
@@ -717,21 +3150,29 @@ export class StripeProjectsAdapter {
717
3150
  }
718
3151
  const resData = (addRes.data || {});
719
3152
  const envVarKeys = Object.keys(secrets);
3153
+ const cliOutput = {
3154
+ provider: "stripe_projects",
3155
+ action: "provision",
3156
+ tool: tool.slug,
3157
+ upstreamService: tool.upstreamSlug,
3158
+ resourceName,
3159
+ provisionedPlan: provisionedPlan || (isPaidPlan ? serviceId : parentPlans[0] || "free"),
3160
+ billingTier: isPaidPlan ? "paid" : "free",
3161
+ fundingSource: hasCustomerStripeByok ? "customer_byok_stripe" : "muxo_master_wallet",
3162
+ envVarKeys,
3163
+ vaultSavedKeys: envVarKeys,
3164
+ secretKeysSaved: envVarKeys,
3165
+ details: resData,
3166
+ };
3167
+ this.attachHostingDeployment({
3168
+ tool,
3169
+ resourceName,
3170
+ input,
3171
+ output: cliOutput,
3172
+ secrets,
3173
+ });
720
3174
  return {
721
- output: {
722
- provider: "stripe_projects",
723
- action: "provision",
724
- tool: tool.slug,
725
- upstreamService: tool.upstreamSlug,
726
- resourceName,
727
- provisionedPlan: provisionedPlan || (isPaidPlan ? serviceId : parentPlans[0] || "free"),
728
- billingTier: isPaidPlan ? "paid" : "free",
729
- fundingSource: hasCustomerStripeByok ? "customer_byok_stripe" : "muxo_master_wallet",
730
- envVarKeys,
731
- vaultSavedKeys: envVarKeys,
732
- secretKeysSaved: envVarKeys,
733
- details: resData,
734
- },
3175
+ output: cliOutput,
735
3176
  provisionedResource: {
736
3177
  resourceName,
737
3178
  providerSlug: tool.providerSlug,