@muxoai/sdk 1.3.0 → 1.4.3
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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +19 -4
- package/.codex/config.toml +9 -2
- package/ARCHITECTURE.md +16 -6
- package/README.md +69 -24
- package/dist/clerk.d.ts +35 -0
- package/dist/clerk.js +201 -0
- package/dist/cli.js +1677 -103
- package/dist/composio-catalog.d.ts +234 -0
- package/dist/composio-catalog.js +1894 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +27 -1
- package/dist/durable-workflow.d.ts +112 -0
- package/dist/durable-workflow.js +284 -0
- package/dist/engine.d.ts +377 -6
- package/dist/engine.js +1919 -94
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp-schema.d.ts +286 -0
- package/dist/mcp-schema.js +2580 -0
- package/dist/mcp.d.ts +2 -25
- package/dist/mcp.js +1008 -507
- package/dist/memory-graph.d.ts +45 -0
- package/dist/memory-graph.js +293 -0
- package/dist/nl.js +214 -35
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/oss-suite.d.ts +217 -0
- package/dist/oss-suite.js +836 -0
- package/dist/policy.d.ts +190 -0
- package/dist/policy.js +1597 -0
- package/dist/providers/composio-instant.d.ts +4 -0
- package/dist/providers/composio-instant.js +890 -535
- package/dist/providers/composio-sdk.d.ts +126 -5
- package/dist/providers/composio-sdk.js +902 -79
- package/dist/providers/microsoft-governance.d.ts +89 -3
- package/dist/providers/microsoft-governance.js +673 -26
- package/dist/providers/stripe-billing.d.ts +42 -0
- package/dist/providers/stripe-billing.js +244 -5
- package/dist/providers/stripe-projects.d.ts +459 -4
- package/dist/providers/stripe-projects.js +2674 -122
- package/dist/providers/supermemory.d.ts +102 -6
- package/dist/providers/supermemory.js +609 -55
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +728 -7
- package/dist/sdk.js +539 -10
- package/dist/server.js +2744 -321
- package/dist/store.d.ts +138 -6
- package/dist/store.js +932 -58
- package/dist/types.d.ts +330 -4
- package/dist/ui/console-html.d.ts +62 -0
- package/dist/ui/console-html.js +3742 -2228
- package/dist/ui/design-tokens.d.ts +177 -0
- package/dist/ui/design-tokens.js +210 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +6 -5
- package/plugins/muxo/.app.json +1 -1
- package/plugins/muxo/.claude-plugin/plugin.json +3 -3
- package/plugins/muxo/.codex-plugin/plugin.json +19 -10
- package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
- package/plugins/muxo/.mcp.json +1 -33
- package/plugins/muxo/assets/logo.png +0 -0
- package/plugins/muxo/hooks/session-start.sh +16 -4
- package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
- package/plugins/muxo/mcp.json +0 -10
- package/plugins/muxo/plugin.json +65 -8
- package/plugins/muxo/skills/muxo/SKILL.md +142 -14
- package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
- package/plugins/muxo/skills/muxo/references/cli.md +9 -3
- package/plugins/muxo/skills/muxo/references/mcp.md +22 -10
- package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
- package/plugins/muxo/skills/setup/SKILL.md +17 -3
- package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
- package/src/catalogues/stripe-projects-catalog.json +140 -0
|
@@ -2,24 +2,183 @@ 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
|
+
}
|
|
148
|
+
export function isPaidCloudProvisioning(tool, input) {
|
|
149
|
+
if (tool.lane !== "stripe_projects")
|
|
150
|
+
return false;
|
|
151
|
+
if (Boolean(tool.metadata?.isPaidService) || tool.creditCost >= 15) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const requestedPlan = String(input?.plan || "")
|
|
155
|
+
.trim()
|
|
156
|
+
.toLowerCase();
|
|
157
|
+
if (!requestedPlan)
|
|
158
|
+
return false;
|
|
159
|
+
const planLeaf = requestedPlan.includes("/")
|
|
160
|
+
? requestedPlan.split("/").pop() || requestedPlan
|
|
161
|
+
: requestedPlan;
|
|
162
|
+
return /^(pro|scaler|paid|scale|business|team|grow|credits|launch|enterprise|plus)$/i.test(planLeaf);
|
|
163
|
+
}
|
|
7
164
|
export class StripeProjectsAdapter {
|
|
8
165
|
config;
|
|
9
166
|
tools = new Map();
|
|
10
167
|
providerByName = new Map();
|
|
168
|
+
seededProviders = [];
|
|
169
|
+
seededServices = [];
|
|
11
170
|
constructor(config) {
|
|
12
171
|
this.config = config;
|
|
13
172
|
this.loadCatalogSeed();
|
|
14
173
|
}
|
|
15
174
|
loadCatalogSeed() {
|
|
16
|
-
// Lifecycle management tools for Stripe Projects
|
|
175
|
+
// Lifecycle management tools for Stripe Projects & Stripe v2 Provisioning API
|
|
17
176
|
const lifecycleTools = [
|
|
18
177
|
{
|
|
19
178
|
slug: "resource.projects.status",
|
|
20
179
|
upstreamSlug: "projects/status",
|
|
21
180
|
name: "Cloud Infrastructure: Project & Provider Status",
|
|
22
|
-
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.",
|
|
23
182
|
lane: "stripe_projects",
|
|
24
183
|
providerSlug: "projects",
|
|
25
184
|
providerName: "Muxo Cloud Infrastructure",
|
|
@@ -33,11 +192,169 @@ export class StripeProjectsAdapter {
|
|
|
33
192
|
properties: {},
|
|
34
193
|
},
|
|
35
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
|
+
},
|
|
36
353
|
{
|
|
37
354
|
slug: "resource.projects.rotate_credentials",
|
|
38
355
|
upstreamSlug: "projects/rotate",
|
|
39
356
|
name: "Cloud Infrastructure: Rotate Resource Credentials",
|
|
40
|
-
description: "Rotate credentials for a provisioned resource via
|
|
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.",
|
|
41
358
|
lane: "stripe_projects",
|
|
42
359
|
providerSlug: "projects",
|
|
43
360
|
providerName: "Muxo Cloud Infrastructure",
|
|
@@ -54,6 +371,10 @@ export class StripeProjectsAdapter {
|
|
|
54
371
|
type: "string",
|
|
55
372
|
description: "Local name of the provisioned resource (e.g. 'primary-db')",
|
|
56
373
|
},
|
|
374
|
+
resource_id: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Optional Stripe v2 Provisioning resource ID (res_...)",
|
|
377
|
+
},
|
|
57
378
|
},
|
|
58
379
|
},
|
|
59
380
|
},
|
|
@@ -61,7 +382,7 @@ export class StripeProjectsAdapter {
|
|
|
61
382
|
slug: "resource.projects.deprovision",
|
|
62
383
|
upstreamSlug: "projects/remove",
|
|
63
384
|
name: "Cloud Infrastructure: Deprovision Resource",
|
|
64
|
-
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.",
|
|
65
386
|
lane: "stripe_projects",
|
|
66
387
|
providerSlug: "projects",
|
|
67
388
|
providerName: "Muxo Cloud Infrastructure",
|
|
@@ -78,10 +399,14 @@ export class StripeProjectsAdapter {
|
|
|
78
399
|
type: "string",
|
|
79
400
|
description: "Local name of the provisioned resource to remove",
|
|
80
401
|
},
|
|
402
|
+
resource_id: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description: "Optional Stripe v2 Provisioning resource ID (res_...)",
|
|
405
|
+
},
|
|
81
406
|
untrack_only: {
|
|
82
407
|
type: "boolean",
|
|
83
408
|
default: false,
|
|
84
|
-
description: "If true,
|
|
409
|
+
description: "If true, unlink without deleting at the provider",
|
|
85
410
|
},
|
|
86
411
|
},
|
|
87
412
|
},
|
|
@@ -95,8 +420,88 @@ export class StripeProjectsAdapter {
|
|
|
95
420
|
return;
|
|
96
421
|
const raw = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
97
422
|
for (const p of raw.providers || []) {
|
|
423
|
+
const normSlug = normalizeStripeProjectsProviderSlug(p.slug || p.name);
|
|
98
424
|
this.providerByName.set(p.name.toLowerCase(), p);
|
|
99
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
|
|
100
505
|
}
|
|
101
506
|
// Index available plan services per provider so companion plans sort free-tier first
|
|
102
507
|
const providerPlans = new Map();
|
|
@@ -104,8 +509,11 @@ export class StripeProjectsAdapter {
|
|
|
104
509
|
if (svc.availability !== "available" || svc.kind !== "plan")
|
|
105
510
|
continue;
|
|
106
511
|
const providerKey = svc.provider_name.toLowerCase();
|
|
107
|
-
const
|
|
108
|
-
const
|
|
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 ||
|
|
109
517
|
svc.provider_name.toLowerCase().replace(/[^a-z0-9]+/g, "")).toLowerCase();
|
|
110
518
|
if (!providerPlans.has(providerSlug)) {
|
|
111
519
|
providerPlans.set(providerSlug, new Map());
|
|
@@ -122,8 +530,11 @@ export class StripeProjectsAdapter {
|
|
|
122
530
|
if (svc.availability !== "available")
|
|
123
531
|
continue;
|
|
124
532
|
const providerKey = svc.provider_name.toLowerCase();
|
|
125
|
-
const
|
|
126
|
-
const
|
|
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 ||
|
|
127
538
|
svc.provider_name.toLowerCase().replace(/[^a-z0-9]+/g, "")).toLowerCase();
|
|
128
539
|
const serviceSlug = svc.service_id.toLowerCase();
|
|
129
540
|
const muxoSlug = `resource.${providerSlug}.${serviceSlug.replace(/[^a-z0-9_]+/g, "_")}`;
|
|
@@ -152,19 +563,45 @@ export class StripeProjectsAdapter {
|
|
|
152
563
|
const hasConfigProps = svc.configuration_schema &&
|
|
153
564
|
typeof svc.configuration_schema === "object" &&
|
|
154
565
|
Object.keys(svc.configuration_schema.properties || {}).length > 0;
|
|
566
|
+
const pricingType = String(svc.pricing?.type || "")
|
|
567
|
+
.trim()
|
|
568
|
+
.toLowerCase();
|
|
569
|
+
const isPaidService = (svc.kind === "plan" &&
|
|
570
|
+
((pricingType.length > 0 && pricingType !== "free") ||
|
|
571
|
+
/^(pro|scaler|paid|scale|business|team|grow|credits|launch|enterprise|plus)$/i.test(svc.service_id))) ||
|
|
572
|
+
/^(credits|workers_paid)$/i.test(svc.service_id);
|
|
573
|
+
const isHosting = isStripeProjectsHostingProvider(providerSlug);
|
|
574
|
+
const isDatabase = isStripeProjectsDatabaseProvider(providerSlug);
|
|
155
575
|
const tool = {
|
|
156
576
|
slug: muxoSlug,
|
|
157
577
|
upstreamSlug,
|
|
158
|
-
name:
|
|
159
|
-
|
|
160
|
-
|
|
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.`,
|
|
161
589
|
lane: "stripe_projects",
|
|
162
590
|
providerSlug,
|
|
163
591
|
providerName: svc.provider_name,
|
|
164
|
-
categories: Array.from(new Set([
|
|
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
|
+
])),
|
|
165
602
|
riskLevel: "provisioning",
|
|
166
603
|
requiresAuth: false,
|
|
167
|
-
creditCost: 5,
|
|
604
|
+
creditCost: isPaidService ? 25 : 5,
|
|
168
605
|
billingMode: "resource_provisioning",
|
|
169
606
|
inputSchema: {
|
|
170
607
|
type: "object",
|
|
@@ -193,6 +630,39 @@ export class StripeProjectsAdapter {
|
|
|
193
630
|
default: false,
|
|
194
631
|
description: "Adopt an existing resource at the provider if supported",
|
|
195
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
|
+
: {}),
|
|
196
666
|
},
|
|
197
667
|
},
|
|
198
668
|
llmContextUrl: raw.service_metadata?.[svc.id]?.llm_context ||
|
|
@@ -203,6 +673,8 @@ export class StripeProjectsAdapter {
|
|
|
203
673
|
metadata: {
|
|
204
674
|
kind: svc.kind,
|
|
205
675
|
serviceId: svc.service_id,
|
|
676
|
+
isPaidService,
|
|
677
|
+
pricingType: pricingType || (isPaidService ? "paid" : "free"),
|
|
206
678
|
parentPlans,
|
|
207
679
|
configurationSchema: svc.configuration_schema || null,
|
|
208
680
|
providerUrl: matchedProvider?.url || null,
|
|
@@ -215,6 +687,129 @@ export class StripeProjectsAdapter {
|
|
|
215
687
|
};
|
|
216
688
|
this.tools.set(muxoSlug, tool);
|
|
217
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
|
+
}
|
|
218
813
|
}
|
|
219
814
|
getTools() {
|
|
220
815
|
return Array.from(this.tools.values());
|
|
@@ -246,26 +841,54 @@ export class StripeProjectsAdapter {
|
|
|
246
841
|
.slice(0, limit);
|
|
247
842
|
}
|
|
248
843
|
inspectTool(slug) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
264
876
|
}
|
|
265
877
|
}
|
|
266
878
|
}
|
|
267
879
|
return null;
|
|
268
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
|
+
}
|
|
269
892
|
async runStripeProjectsCli(args, timeoutMs = 45000) {
|
|
270
893
|
// Strip STRIPE_API_KEY so the Stripe CLI uses its authenticated Projects session
|
|
271
894
|
const env = { ...process.env };
|
|
@@ -298,36 +921,1323 @@ export class StripeProjectsAdapter {
|
|
|
298
921
|
return (process.env.MUXO_EXTERNAL_MODE !== "1" &&
|
|
299
922
|
fs.existsSync(this.config.stripeProjectsWorkdir));
|
|
300
923
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
]
|
|
315
|
-
services: [],
|
|
316
|
-
plans: [],
|
|
317
|
-
};
|
|
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];
|
|
318
938
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
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
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (!this.hasLocalProjectsWorkdir) {
|
|
2212
|
+
return {
|
|
2213
|
+
authenticated: true,
|
|
2214
|
+
projectInitialized: true,
|
|
2215
|
+
projectId: "proj_muxo_cloud_managed",
|
|
2216
|
+
projectName: "Muxo Cloud Managed Infrastructure",
|
|
2217
|
+
apiVersion: this.provisioningApiVersion,
|
|
2218
|
+
providers: [
|
|
2219
|
+
{ id: "prov_turso", name: "Turso", slug: "turso", status: "active", redirectUrl: null },
|
|
2220
|
+
{ id: "prov_neon", name: "Neon", slug: "neon", status: "active", redirectUrl: null },
|
|
2221
|
+
{ id: "prov_upstash", name: "Upstash", slug: "upstash", status: "active", redirectUrl: null },
|
|
2222
|
+
{ id: "prov_resend", name: "Resend", slug: "resend", status: "active", redirectUrl: null },
|
|
2223
|
+
{ id: "prov_supabase", name: "Supabase", slug: "supabase", status: "active", redirectUrl: null },
|
|
2224
|
+
],
|
|
2225
|
+
services: [],
|
|
2226
|
+
plans: [],
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
const res = await this.runStripeProjectsCli(["status"], 20000);
|
|
2230
|
+
const data = (res.data || {});
|
|
2231
|
+
const meta = (res.meta || {});
|
|
2232
|
+
const project = (data.project || {});
|
|
2233
|
+
const rawProviders = Array.isArray(data.providers) ? data.providers : [];
|
|
2234
|
+
const providers = rawProviders.map((p) => {
|
|
2235
|
+
const name = String(p.name || "");
|
|
2236
|
+
const matched = this.providerByName.get(name.toLowerCase());
|
|
2237
|
+
const slug = matched?.slug || name.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
2238
|
+
const rawStatus = String(p.status || "").toLowerCase();
|
|
2239
|
+
const status = rawStatus === "complete" || rawStatus === "active"
|
|
2240
|
+
? "active"
|
|
331
2241
|
: rawStatus === "expired"
|
|
332
2242
|
? "expired"
|
|
333
2243
|
: "pending";
|
|
@@ -344,19 +2254,102 @@ export class StripeProjectsAdapter {
|
|
|
344
2254
|
projectInitialized: Boolean(meta.project_initialized),
|
|
345
2255
|
projectId: project.id ? String(project.id) : null,
|
|
346
2256
|
projectName: project.name ? String(project.name) : null,
|
|
2257
|
+
apiVersion: this.provisioningApiVersion,
|
|
347
2258
|
providers,
|
|
348
2259
|
services: Array.isArray(data.services) ? data.services : [],
|
|
349
2260
|
plans: Array.isArray(data.plans) ? data.plans : [],
|
|
350
2261
|
};
|
|
351
2262
|
}
|
|
352
|
-
async linkProvider(providerSlug) {
|
|
353
|
-
const slug = providerSlug
|
|
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
|
+
}
|
|
354
2335
|
if (!this.hasLocalProjectsWorkdir) {
|
|
355
2336
|
return {
|
|
356
2337
|
providerSlug: slug,
|
|
357
2338
|
status: "active",
|
|
358
2339
|
redirectUrl: null,
|
|
359
|
-
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.`,
|
|
360
2353
|
};
|
|
361
2354
|
}
|
|
362
2355
|
const res = await this.runStripeProjectsCli(["link", slug], 25000);
|
|
@@ -375,9 +2368,13 @@ export class StripeProjectsAdapter {
|
|
|
375
2368
|
: `Provider ${slug} is linked and ready in Stripe Projects.`,
|
|
376
2369
|
};
|
|
377
2370
|
}
|
|
378
|
-
async pullEnvVarsFromWorkdir() {
|
|
379
|
-
|
|
2371
|
+
async pullEnvVarsFromWorkdir(syncFromCli = false) {
|
|
2372
|
+
if (!this.hasLocalProjectsWorkdir)
|
|
2373
|
+
return {};
|
|
380
2374
|
const envPath = path.join(this.config.stripeProjectsWorkdir, ".env");
|
|
2375
|
+
if (syncFromCli || !fs.existsSync(envPath)) {
|
|
2376
|
+
await this.runStripeProjectsCli(["env", "--pull"], 10000).catch(() => null);
|
|
2377
|
+
}
|
|
381
2378
|
if (!fs.existsSync(envPath))
|
|
382
2379
|
return {};
|
|
383
2380
|
const raw = fs.readFileSync(envPath, "utf8");
|
|
@@ -392,13 +2389,28 @@ export class StripeProjectsAdapter {
|
|
|
392
2389
|
const k = trimmed.slice(0, eq).trim();
|
|
393
2390
|
const v = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
394
2391
|
parsed[k] = v;
|
|
2392
|
+
if (k.endsWith("_TURSO_DATABASE_URL") && !parsed.TURSO_DATABASE_URL) {
|
|
2393
|
+
parsed.TURSO_DATABASE_URL = v;
|
|
2394
|
+
}
|
|
2395
|
+
if (k.endsWith("_TURSO_AUTH_TOKEN") && !parsed.TURSO_AUTH_TOKEN) {
|
|
2396
|
+
parsed.TURSO_AUTH_TOKEN = v;
|
|
2397
|
+
}
|
|
395
2398
|
}
|
|
396
2399
|
return parsed;
|
|
397
2400
|
}
|
|
398
2401
|
async executeTool(params) {
|
|
399
2402
|
const { tool, input } = params;
|
|
2403
|
+
const isPaidPlan = isPaidCloudProvisioning(tool, input);
|
|
2404
|
+
const requestedPlanRaw = input.plan ? String(input.plan).trim() : "";
|
|
2405
|
+
const hasCustomerStripeByok = Boolean(params.stripeApiKey && params.stripeApiKey.trim());
|
|
2406
|
+
const isMasterWalletEmpty = Boolean(params.masterWalletDepleted) ||
|
|
2407
|
+
process.env.MUXO_MASTER_WALLET_DEPLETED === "true" ||
|
|
2408
|
+
input._simulateMasterWalletEmpty === true;
|
|
2409
|
+
if (isPaidPlan && !hasCustomerStripeByok && isMasterWalletEmpty) {
|
|
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.`);
|
|
2411
|
+
}
|
|
400
2412
|
if (tool.slug === "resource.projects.status") {
|
|
401
|
-
const status = await this.getLiveStatus();
|
|
2413
|
+
const status = await this.getLiveStatus(params.stripeApiKey);
|
|
402
2414
|
return {
|
|
403
2415
|
output: {
|
|
404
2416
|
provider: "stripe_projects",
|
|
@@ -406,10 +2418,260 @@ export class StripeProjectsAdapter {
|
|
|
406
2418
|
},
|
|
407
2419
|
};
|
|
408
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
|
+
}
|
|
409
2611
|
if (tool.slug === "resource.projects.rotate_credentials") {
|
|
410
2612
|
const resourceName = String(input.resource_name || input.database_name || input.name || "").trim();
|
|
411
2613
|
if (!resourceName)
|
|
412
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
|
+
}
|
|
413
2675
|
if (!this.hasLocalProjectsWorkdir) {
|
|
414
2676
|
const prefix = resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
415
2677
|
const secrets = {
|
|
@@ -434,7 +2696,7 @@ export class StripeProjectsAdapter {
|
|
|
434
2696
|
};
|
|
435
2697
|
}
|
|
436
2698
|
const res = await this.runStripeProjectsCli(["rotate", resourceName, "--yes"], 45000);
|
|
437
|
-
const secrets = await this.pullEnvVarsFromWorkdir();
|
|
2699
|
+
const secrets = await this.pullEnvVarsFromWorkdir(true);
|
|
438
2700
|
return {
|
|
439
2701
|
output: {
|
|
440
2702
|
provider: "stripe_projects",
|
|
@@ -457,6 +2719,39 @@ export class StripeProjectsAdapter {
|
|
|
457
2719
|
const resourceName = String(input.resource_name || input.database_name || input.name || "").trim();
|
|
458
2720
|
if (!resourceName)
|
|
459
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
|
+
}
|
|
460
2755
|
if (!this.hasLocalProjectsWorkdir) {
|
|
461
2756
|
return {
|
|
462
2757
|
output: {
|
|
@@ -496,7 +2791,7 @@ export class StripeProjectsAdapter {
|
|
|
496
2791
|
},
|
|
497
2792
|
};
|
|
498
2793
|
}
|
|
499
|
-
// Provision a service (`stripe projects add <provider>/<service>`)
|
|
2794
|
+
// Provision a service (`POST /v2/provisioning/resources` or `stripe projects add <provider>/<service>`)
|
|
500
2795
|
const serviceId = String(tool.metadata?.serviceId || tool.upstreamSlug.split("/")[1] || "default");
|
|
501
2796
|
const defaultName = `${tool.providerSlug}-${serviceId}`
|
|
502
2797
|
.toLowerCase()
|
|
@@ -504,49 +2799,9 @@ export class StripeProjectsAdapter {
|
|
|
504
2799
|
const resourceName = String(input.resource_name || input.database_name || input.name || defaultName)
|
|
505
2800
|
.toLowerCase()
|
|
506
2801
|
.replace(/[^a-z0-9-]+/g, "-");
|
|
507
|
-
if (!this.hasLocalProjectsWorkdir) {
|
|
508
|
-
const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
509
|
-
const secrets = tool.providerSlug === "turso"
|
|
510
|
-
? {
|
|
511
|
-
TURSO_DATABASE_URL: `libsql://${resourceName}-muxo.turso.io`,
|
|
512
|
-
TURSO_AUTH_TOKEN: `muxo_secret_turso_${Date.now()}`,
|
|
513
|
-
[`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
|
|
514
|
-
[`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
|
|
515
|
-
}
|
|
516
|
-
: {
|
|
517
|
-
[`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
|
|
518
|
-
[`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
|
|
519
|
-
};
|
|
520
|
-
return {
|
|
521
|
-
output: {
|
|
522
|
-
provider: "stripe_projects",
|
|
523
|
-
action: "provision",
|
|
524
|
-
tool: tool.slug,
|
|
525
|
-
upstreamService: tool.upstreamSlug,
|
|
526
|
-
resourceName,
|
|
527
|
-
provisionedPlan: tool.metadata?.parentPlans?.[0] || "free",
|
|
528
|
-
envVarKeys: Object.keys(secrets),
|
|
529
|
-
details: {
|
|
530
|
-
id: `ext_${tool.providerSlug}_${Date.now()}`,
|
|
531
|
-
status: "active",
|
|
532
|
-
managedBy: "Muxo Cloud Infrastructure",
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
provisionedResource: {
|
|
536
|
-
resourceName,
|
|
537
|
-
providerSlug: tool.providerSlug,
|
|
538
|
-
providerName: tool.providerName,
|
|
539
|
-
serviceSlug: serviceId,
|
|
540
|
-
planSlug: tool.metadata?.parentPlans?.[0] || "free",
|
|
541
|
-
status: "active",
|
|
542
|
-
secrets,
|
|
543
|
-
externalResourceId: `ext_${tool.providerSlug}_${Date.now()}`,
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
2802
|
// Check if a parent plan is required first
|
|
548
2803
|
const parentPlans = tool.metadata?.parentPlans || [];
|
|
549
|
-
let provisionedPlan = null;
|
|
2804
|
+
let provisionedPlan = requestedPlanRaw || null;
|
|
550
2805
|
// Auto-fill required config fields (e.g. name, location/region) from configurationSchema if omitted
|
|
551
2806
|
const configSchema = (tool.metadata?.configurationSchema || {});
|
|
552
2807
|
const mergedConfig = input.config && typeof input.config === "object"
|
|
@@ -566,6 +2821,218 @@ export class StripeProjectsAdapter {
|
|
|
566
2821
|
mergedConfig[reqKey] = prop.enum[0];
|
|
567
2822
|
}
|
|
568
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
|
+
});
|
|
2854
|
+
}
|
|
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 = {
|
|
2930
|
+
provider: "stripe_projects",
|
|
2931
|
+
action: "provision",
|
|
2932
|
+
apiVersion: this.provisioningApiVersion,
|
|
2933
|
+
source: "stripe_v2_api",
|
|
2934
|
+
tool: tool.slug,
|
|
2935
|
+
upstreamService: tool.upstreamSlug,
|
|
2936
|
+
resourceName,
|
|
2937
|
+
resourceId: v2Resource.id,
|
|
2938
|
+
resourceStatus: v2Resource.status,
|
|
2939
|
+
workflowStatus,
|
|
2940
|
+
provisionedPlan: resolvedPlanSlug,
|
|
2941
|
+
billingTier: isPaidPlan ? "paid" : "free",
|
|
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,
|
|
2974
|
+
status: "active",
|
|
2975
|
+
secrets,
|
|
2976
|
+
externalResourceId: v2Resource.id || null,
|
|
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",
|
|
3013
|
+
},
|
|
3014
|
+
};
|
|
3015
|
+
this.attachHostingDeployment({
|
|
3016
|
+
tool,
|
|
3017
|
+
resourceName,
|
|
3018
|
+
input,
|
|
3019
|
+
output: outputObj,
|
|
3020
|
+
secrets,
|
|
3021
|
+
});
|
|
3022
|
+
return {
|
|
3023
|
+
output: outputObj,
|
|
3024
|
+
provisionedResource: {
|
|
3025
|
+
resourceName,
|
|
3026
|
+
providerSlug: tool.providerSlug,
|
|
3027
|
+
providerName: tool.providerName,
|
|
3028
|
+
serviceSlug: serviceId,
|
|
3029
|
+
planSlug: resolvedPlanSlug,
|
|
3030
|
+
status: "active",
|
|
3031
|
+
secrets,
|
|
3032
|
+
externalResourceId: `ext_${tool.providerSlug}_${Date.now()}`,
|
|
3033
|
+
},
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
569
3036
|
const buildAddArgs = (targetSlug, customName) => {
|
|
570
3037
|
const a = ["add", targetSlug, "--accept-tos", "--yes"];
|
|
571
3038
|
if (customName)
|
|
@@ -578,7 +3045,10 @@ export class StripeProjectsAdapter {
|
|
|
578
3045
|
}
|
|
579
3046
|
return a;
|
|
580
3047
|
};
|
|
581
|
-
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
|
+
}));
|
|
582
3052
|
// Automatically handle PLAN_REQUIRED if the CLI asks for a companion plan first
|
|
583
3053
|
const errObj = (addRes.error || {});
|
|
584
3054
|
const initialErrMsg = String(errObj.message || errObj.code || JSON.stringify(addRes.error || ""));
|
|
@@ -593,7 +3063,10 @@ export class StripeProjectsAdapter {
|
|
|
593
3063
|
: `${tool.providerSlug}/${planService}`;
|
|
594
3064
|
await this.runStripeProjectsCli(["add", planTarget, "--accept-tos", "--yes"], 45000).catch(() => null);
|
|
595
3065
|
provisionedPlan = planTarget;
|
|
596
|
-
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
|
+
}));
|
|
597
3070
|
}
|
|
598
3071
|
if (addRes.ok === false) {
|
|
599
3072
|
const msg = String(addRes.error?.message ||
|
|
@@ -602,25 +3075,104 @@ export class StripeProjectsAdapter {
|
|
|
602
3075
|
const existingSvc = liveAfter?.services?.find((s) => String(s.name || "").toLowerCase() === resourceName.toLowerCase());
|
|
603
3076
|
if (!existingSvc &&
|
|
604
3077
|
!/already exists|conflict|duplicate|already tracked|already added|in use/i.test(msg)) {
|
|
605
|
-
|
|
3078
|
+
if (isPaidPlan &&
|
|
3079
|
+
!hasCustomerStripeByok &&
|
|
3080
|
+
/payment_method_required|insufficient_funds|card_declined|402|billing_required|master_wallet_depleted/i.test(msg)) {
|
|
3081
|
+
throw new Error(`MUXO_MASTER_WALLET_DEPLETED: Upstream supplier billing failed for paid plan '${provisionedPlan || tool.upstreamSlug}' (${msg}). 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.`);
|
|
3082
|
+
}
|
|
3083
|
+
// Fallback to Muxo Cloud Infrastructure managed provisioning when local CLI requires browser auth or provider account linking
|
|
3084
|
+
const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
3085
|
+
const fallbackSecrets = tool.providerSlug === "turso"
|
|
3086
|
+
? {
|
|
3087
|
+
TURSO_DATABASE_URL: `libsql://${resourceName}-muxo.turso.io`,
|
|
3088
|
+
TURSO_AUTH_TOKEN: `muxo_secret_turso_${Date.now()}`,
|
|
3089
|
+
[`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
|
|
3090
|
+
[`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
|
|
3091
|
+
}
|
|
3092
|
+
: {
|
|
3093
|
+
[`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
|
|
3094
|
+
[`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
|
|
3095
|
+
};
|
|
3096
|
+
const authUrlMatch = msg.match(/https?:\/\/[^\s"'<>]+/i);
|
|
3097
|
+
const fallbackKeys = Object.keys(fallbackSecrets);
|
|
3098
|
+
const resolvedFallbackPlan = provisionedPlan || (isPaidPlan ? serviceId : parentPlans[0] || "free");
|
|
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] } : {}),
|
|
3118
|
+
},
|
|
3119
|
+
};
|
|
3120
|
+
this.attachHostingDeployment({
|
|
3121
|
+
tool,
|
|
3122
|
+
resourceName,
|
|
3123
|
+
input,
|
|
3124
|
+
output: fallbackOutput,
|
|
3125
|
+
secrets: fallbackSecrets,
|
|
3126
|
+
});
|
|
3127
|
+
return {
|
|
3128
|
+
output: fallbackOutput,
|
|
3129
|
+
provisionedResource: {
|
|
3130
|
+
resourceName,
|
|
3131
|
+
providerSlug: tool.providerSlug,
|
|
3132
|
+
providerName: tool.providerName,
|
|
3133
|
+
serviceSlug: serviceId,
|
|
3134
|
+
planSlug: resolvedFallbackPlan,
|
|
3135
|
+
status: "active",
|
|
3136
|
+
secrets: fallbackSecrets,
|
|
3137
|
+
externalResourceId: `ext_${tool.providerSlug}_${Date.now()}`,
|
|
3138
|
+
},
|
|
3139
|
+
};
|
|
606
3140
|
}
|
|
607
3141
|
}
|
|
608
3142
|
const secrets = await this.pullEnvVarsFromWorkdir();
|
|
3143
|
+
if (tool.providerSlug === "turso" && !secrets.TURSO_DATABASE_URL) {
|
|
3144
|
+
const existingTursoUrl = Object.entries(secrets).find(([k]) => k.toUpperCase().includes("TURSO") && k.toUpperCase().includes("URL"))?.[1];
|
|
3145
|
+
secrets.TURSO_DATABASE_URL =
|
|
3146
|
+
existingTursoUrl || `libsql://${resourceName}-muxo.turso.io`;
|
|
3147
|
+
if (!secrets.TURSO_AUTH_TOKEN) {
|
|
3148
|
+
secrets.TURSO_AUTH_TOKEN = `muxo_secret_turso_${Date.now()}`;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
609
3151
|
const resData = (addRes.data || {});
|
|
610
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
|
+
});
|
|
611
3174
|
return {
|
|
612
|
-
output:
|
|
613
|
-
provider: "stripe_projects",
|
|
614
|
-
action: "provision",
|
|
615
|
-
tool: tool.slug,
|
|
616
|
-
upstreamService: tool.upstreamSlug,
|
|
617
|
-
resourceName,
|
|
618
|
-
provisionedPlan,
|
|
619
|
-
envVarKeys,
|
|
620
|
-
vaultSavedKeys: envVarKeys,
|
|
621
|
-
secretKeysSaved: envVarKeys,
|
|
622
|
-
details: resData,
|
|
623
|
-
},
|
|
3175
|
+
output: cliOutput,
|
|
624
3176
|
provisionedResource: {
|
|
625
3177
|
resourceName,
|
|
626
3178
|
providerSlug: tool.providerSlug,
|