@opengeni/api-router 0.5.3 → 0.5.5
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/dist/app.d.ts +9 -1
- package/dist/app.js +7 -1
- package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
- package/dist/chunk-HBEJMWD3.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +279 -55
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
- package/src/app.ts +583 -166
- package/src/auth/managed-auth.ts +32 -16
- package/src/http/auth.ts +8 -1
- package/src/http/common.ts +6 -2
- package/src/http/sse.ts +84 -8
- package/src/index.ts +178 -75
- package/src/integrations/oauth-client.ts +403 -120
- package/src/integrations/provider-domain.ts +4 -1
- package/src/mcp/documents.ts +173 -94
- package/src/mcp/server.ts +1600 -693
- package/src/mcp/session-view.ts +8 -2
- package/src/mcp/toolspace.ts +175 -84
- package/src/observability.ts +7 -1
- package/src/routes/api-keys.ts +39 -23
- package/src/routes/billing.ts +180 -65
- package/src/routes/capabilities.ts +17 -8
- package/src/routes/catalog-assets.ts +5 -2
- package/src/routes/codex.ts +244 -63
- package/src/routes/connections.ts +71 -33
- package/src/routes/documents.ts +242 -92
- package/src/routes/enrollments.ts +100 -70
- package/src/routes/environments.ts +205 -136
- package/src/routes/files.ts +164 -39
- package/src/routes/github.ts +123 -50
- package/src/routes/install.ts +9 -2
- package/src/routes/machines.ts +9 -8
- package/src/routes/packs.ts +141 -89
- package/src/routes/rigs.ts +189 -0
- package/src/routes/scheduled-tasks.ts +51 -9
- package/src/routes/sessions.ts +870 -328
- package/src/routes/social.ts +50 -38
- package/src/routes/workspace-capture.ts +238 -0
- package/src/routes/workspaces.ts +146 -13
- package/src/sandbox/access.ts +11 -3
- package/src/sandbox/auth-callout.ts +5 -1
- package/src/sandbox/channel-a.ts +104 -27
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +68 -59
- package/src/sandbox/metrics-ingestion.ts +238 -17
- package/src/sandbox/viewer.ts +172 -46
- package/dist/chunk-3HIA43CC.js.map +0 -1
package/src/routes/billing.ts
CHANGED
|
@@ -27,14 +27,22 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
27
27
|
app.get("/v1/billing", async (c) => {
|
|
28
28
|
const context = await requireAccessContext(c, deps);
|
|
29
29
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
30
|
-
return c.json({
|
|
30
|
+
return c.json({
|
|
31
|
+
mode: deps.settings.billingMode,
|
|
32
|
+
balance: await getBillingBalance(deps.db, accountId),
|
|
33
|
+
});
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
app.get("/v1/billing/usage", async (c) => {
|
|
34
37
|
const context = await requireAccessContext(c, deps);
|
|
35
38
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
36
39
|
const workspaceId = c.req.query("workspaceId");
|
|
37
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
workspaceId &&
|
|
42
|
+
!context.workspaceGrants.some(
|
|
43
|
+
(grant) => grant.accountId === accountId && grant.workspaceId === workspaceId,
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
38
46
|
throw new HTTPException(403, { message: "missing workspace access for usage query" });
|
|
39
47
|
}
|
|
40
48
|
return c.json({
|
|
@@ -50,7 +58,11 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
50
58
|
app.get("/v1/billing/entitlements", async (c) => {
|
|
51
59
|
const context = await requireAccessContext(c, deps);
|
|
52
60
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
53
|
-
return c.json({
|
|
61
|
+
return c.json({
|
|
62
|
+
accountId,
|
|
63
|
+
mode: deps.settings.entitlementsMode,
|
|
64
|
+
entitlements: configuredEntitlements(deps.settings),
|
|
65
|
+
});
|
|
54
66
|
});
|
|
55
67
|
|
|
56
68
|
app.post("/v1/billing/checkout", async (c) => {
|
|
@@ -60,7 +72,9 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
60
72
|
const context = await requireAccessContext(c, deps);
|
|
61
73
|
const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
|
|
62
74
|
if (!parsed.success) {
|
|
63
|
-
throw new HTTPException(400, {
|
|
75
|
+
throw new HTTPException(400, {
|
|
76
|
+
message: parsed.error.issues[0]?.message ?? "invalid checkout request",
|
|
77
|
+
});
|
|
64
78
|
}
|
|
65
79
|
const body = parsed.data;
|
|
66
80
|
const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
|
|
@@ -69,24 +83,29 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
69
83
|
const stripe = stripeClient(deps);
|
|
70
84
|
const customerId = await getOrCreateStripeCustomer(deps, stripe, context, accountId);
|
|
71
85
|
const idempotencyKey = `checkout:${accountId}:${amountMicros}:${crypto.randomUUID()}`;
|
|
72
|
-
const session = await stripe.checkout.sessions.create(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
const session = await stripe.checkout.sessions.create(
|
|
87
|
+
stripeCheckoutSessionCreateParams({
|
|
88
|
+
accountId,
|
|
89
|
+
customerId,
|
|
90
|
+
amountCents,
|
|
91
|
+
amountMicros,
|
|
92
|
+
creditsProductId: deps.settings.stripeCreditsProductId,
|
|
93
|
+
publicBaseUrl: deps.settings.publicBaseUrl,
|
|
94
|
+
successUrl: body.successUrl,
|
|
95
|
+
cancelUrl: body.cancelUrl,
|
|
96
|
+
idempotencyKey,
|
|
97
|
+
}),
|
|
98
|
+
{ idempotencyKey },
|
|
99
|
+
);
|
|
83
100
|
if (!session.url) {
|
|
84
101
|
throw new HTTPException(502, { message: "Stripe did not return a checkout URL" });
|
|
85
102
|
}
|
|
86
|
-
return c.json(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
return c.json(
|
|
104
|
+
CreateCheckoutResponse.parse({
|
|
105
|
+
checkoutSessionId: session.id,
|
|
106
|
+
url: session.url,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
90
109
|
});
|
|
91
110
|
|
|
92
111
|
app.post("/v1/webhooks/stripe", async (c) => {
|
|
@@ -100,9 +119,15 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
100
119
|
const payload = await c.req.text();
|
|
101
120
|
let event: Stripe.Event;
|
|
102
121
|
try {
|
|
103
|
-
event = await stripeClient(deps).webhooks.constructEventAsync(
|
|
122
|
+
event = await stripeClient(deps).webhooks.constructEventAsync(
|
|
123
|
+
payload,
|
|
124
|
+
signature,
|
|
125
|
+
deps.settings.stripeWebhookSecret!,
|
|
126
|
+
);
|
|
104
127
|
} catch (error) {
|
|
105
|
-
throw new HTTPException(400, {
|
|
128
|
+
throw new HTTPException(400, {
|
|
129
|
+
message: error instanceof Error ? error.message : "invalid stripe signature",
|
|
130
|
+
});
|
|
106
131
|
}
|
|
107
132
|
const firstSeen = await recordStripeWebhookEvent(deps.db, {
|
|
108
133
|
id: event.id,
|
|
@@ -120,7 +145,9 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
120
145
|
await markStripeWebhookProcessed(deps.db, event.id);
|
|
121
146
|
return c.json({ received: true });
|
|
122
147
|
} catch (error) {
|
|
123
|
-
throw new HTTPException(500, {
|
|
148
|
+
throw new HTTPException(500, {
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
124
151
|
}
|
|
125
152
|
});
|
|
126
153
|
}
|
|
@@ -136,8 +163,18 @@ export function stripeCheckoutSessionCreateParams(input: {
|
|
|
136
163
|
cancelUrl?: string | undefined;
|
|
137
164
|
idempotencyKey: string;
|
|
138
165
|
}): Stripe.Checkout.SessionCreateParams {
|
|
139
|
-
const successUrl = checkoutReturnUrl(
|
|
140
|
-
|
|
166
|
+
const successUrl = checkoutReturnUrl(
|
|
167
|
+
input.publicBaseUrl,
|
|
168
|
+
input.successUrl,
|
|
169
|
+
"/billing?checkout=success",
|
|
170
|
+
"successUrl",
|
|
171
|
+
);
|
|
172
|
+
const cancelUrl = checkoutReturnUrl(
|
|
173
|
+
input.publicBaseUrl,
|
|
174
|
+
input.cancelUrl,
|
|
175
|
+
"/billing?checkout=cancelled",
|
|
176
|
+
"cancelUrl",
|
|
177
|
+
);
|
|
141
178
|
return {
|
|
142
179
|
mode: "payment",
|
|
143
180
|
customer: input.customerId,
|
|
@@ -149,24 +186,26 @@ export function stripeCheckoutSessionCreateParams(input: {
|
|
|
149
186
|
cancel_url: cancelUrl,
|
|
150
187
|
automatic_tax: { enabled: true },
|
|
151
188
|
billing_address_collection: "auto",
|
|
152
|
-
line_items: [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
line_items: [
|
|
190
|
+
{
|
|
191
|
+
quantity: 1,
|
|
192
|
+
price_data: {
|
|
193
|
+
currency: "usd",
|
|
194
|
+
unit_amount: input.amountCents,
|
|
195
|
+
...(input.creditsProductId
|
|
196
|
+
? { product: input.creditsProductId }
|
|
197
|
+
: {
|
|
198
|
+
product_data: {
|
|
199
|
+
name: "OpenGeni credits",
|
|
200
|
+
metadata: {
|
|
201
|
+
app: "opengeni",
|
|
202
|
+
billing_model: "prepaid_credits",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
168
207
|
},
|
|
169
|
-
|
|
208
|
+
],
|
|
170
209
|
metadata: {
|
|
171
210
|
opengeni_account_id: input.accountId,
|
|
172
211
|
opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
|
|
@@ -184,9 +223,16 @@ export function stripeCheckoutSessionCreateParams(input: {
|
|
|
184
223
|
};
|
|
185
224
|
}
|
|
186
225
|
|
|
187
|
-
function checkoutReturnUrl(
|
|
226
|
+
function checkoutReturnUrl(
|
|
227
|
+
publicBaseUrl: string | undefined,
|
|
228
|
+
candidate: string | undefined,
|
|
229
|
+
fallbackPath: string,
|
|
230
|
+
field: string,
|
|
231
|
+
): string {
|
|
188
232
|
if (!publicBaseUrl) {
|
|
189
|
-
throw new HTTPException(500, {
|
|
233
|
+
throw new HTTPException(500, {
|
|
234
|
+
message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout",
|
|
235
|
+
});
|
|
190
236
|
}
|
|
191
237
|
const base = new URL(publicBaseUrl);
|
|
192
238
|
const fallback = new URL(fallbackPath, base).toString();
|
|
@@ -200,7 +246,11 @@ function checkoutReturnUrl(publicBaseUrl: string | undefined, candidate: string
|
|
|
200
246
|
return parsed.toString();
|
|
201
247
|
}
|
|
202
248
|
|
|
203
|
-
async function handleStripeWebhookEvent(
|
|
249
|
+
async function handleStripeWebhookEvent(
|
|
250
|
+
deps: ApiRouteDeps,
|
|
251
|
+
stripe: Stripe,
|
|
252
|
+
event: Stripe.Event,
|
|
253
|
+
): Promise<void> {
|
|
204
254
|
switch (event.type) {
|
|
205
255
|
case "checkout.session.completed":
|
|
206
256
|
await handleCheckoutSessionCompleted(deps, event);
|
|
@@ -239,7 +289,10 @@ async function handleStripeWebhookEvent(deps: ApiRouteDeps, stripe: Stripe, even
|
|
|
239
289
|
}
|
|
240
290
|
}
|
|
241
291
|
|
|
242
|
-
async function handleCheckoutSessionCompleted(
|
|
292
|
+
async function handleCheckoutSessionCompleted(
|
|
293
|
+
deps: ApiRouteDeps,
|
|
294
|
+
event: Stripe.Event,
|
|
295
|
+
): Promise<void> {
|
|
243
296
|
const session = event.data.object as Stripe.Checkout.Session;
|
|
244
297
|
if (session.mode !== "payment" || session.payment_status !== "paid") {
|
|
245
298
|
return;
|
|
@@ -263,7 +316,10 @@ async function handleCheckoutSessionCompleted(deps: ApiRouteDeps, event: Stripe.
|
|
|
263
316
|
idempotencyKey: credit.idempotencyKey,
|
|
264
317
|
metadata: {
|
|
265
318
|
stripeEventId: event.id,
|
|
266
|
-
stripePaymentIntentId:
|
|
319
|
+
stripePaymentIntentId:
|
|
320
|
+
typeof session.payment_intent === "string"
|
|
321
|
+
? session.payment_intent
|
|
322
|
+
: (session.payment_intent?.id ?? null),
|
|
267
323
|
stripePackageId: credit.packageId,
|
|
268
324
|
stripeCreditAmountUsd: credit.amountUsd,
|
|
269
325
|
},
|
|
@@ -285,18 +341,30 @@ async function mirrorPaymentIntentCustomer(deps: ApiRouteDeps, event: Stripe.Eve
|
|
|
285
341
|
}
|
|
286
342
|
}
|
|
287
343
|
|
|
288
|
-
async function handleChargeRefunded(
|
|
344
|
+
async function handleChargeRefunded(
|
|
345
|
+
deps: ApiRouteDeps,
|
|
346
|
+
stripe: Stripe,
|
|
347
|
+
event: Stripe.Event,
|
|
348
|
+
): Promise<void> {
|
|
289
349
|
const charge = event.data.object as Stripe.Charge;
|
|
290
350
|
for (const refund of charge.refunds?.data ?? []) {
|
|
291
351
|
await applyRefundDebit(deps, stripe, refund);
|
|
292
352
|
}
|
|
293
353
|
}
|
|
294
354
|
|
|
295
|
-
async function handleRefundEvent(
|
|
355
|
+
async function handleRefundEvent(
|
|
356
|
+
deps: ApiRouteDeps,
|
|
357
|
+
stripe: Stripe,
|
|
358
|
+
event: Stripe.Event,
|
|
359
|
+
): Promise<void> {
|
|
296
360
|
await applyRefundDebit(deps, stripe, event.data.object as Stripe.Refund);
|
|
297
361
|
}
|
|
298
362
|
|
|
299
|
-
async function applyRefundDebit(
|
|
363
|
+
async function applyRefundDebit(
|
|
364
|
+
deps: ApiRouteDeps,
|
|
365
|
+
stripe: Stripe,
|
|
366
|
+
refund: Stripe.Refund,
|
|
367
|
+
): Promise<void> {
|
|
300
368
|
if (refund.status && refund.status !== "succeeded") {
|
|
301
369
|
return;
|
|
302
370
|
}
|
|
@@ -324,7 +392,11 @@ async function applyRefundDebit(deps: ApiRouteDeps, stripe: Stripe, refund: Stri
|
|
|
324
392
|
recordCreditMicrosMetric(deps, "refund", centsToMicros(refund.amount));
|
|
325
393
|
}
|
|
326
394
|
|
|
327
|
-
async function holdDisputedCredits(
|
|
395
|
+
async function holdDisputedCredits(
|
|
396
|
+
deps: ApiRouteDeps,
|
|
397
|
+
stripe: Stripe,
|
|
398
|
+
event: Stripe.Event,
|
|
399
|
+
): Promise<void> {
|
|
328
400
|
const dispute = event.data.object as Stripe.Dispute;
|
|
329
401
|
const metadata = await metadataForDispute(stripe, dispute);
|
|
330
402
|
const accountId = metadata?.opengeni_account_id;
|
|
@@ -342,7 +414,11 @@ async function holdDisputedCredits(deps: ApiRouteDeps, stripe: Stripe, event: St
|
|
|
342
414
|
});
|
|
343
415
|
}
|
|
344
416
|
|
|
345
|
-
async function releaseDisputedCredits(
|
|
417
|
+
async function releaseDisputedCredits(
|
|
418
|
+
deps: ApiRouteDeps,
|
|
419
|
+
stripe: Stripe,
|
|
420
|
+
event: Stripe.Event,
|
|
421
|
+
): Promise<void> {
|
|
346
422
|
const dispute = event.data.object as Stripe.Dispute;
|
|
347
423
|
if (event.type === "charge.dispute.closed" && dispute.status !== "won") {
|
|
348
424
|
return;
|
|
@@ -353,7 +429,7 @@ async function releaseDisputedCredits(deps: ApiRouteDeps, stripe: Stripe, event:
|
|
|
353
429
|
return;
|
|
354
430
|
}
|
|
355
431
|
const holdIdempotencyKey = `stripe:dispute_hold:${dispute.id}`;
|
|
356
|
-
if (!await hasCreditLedgerEntry(deps.db, accountId, holdIdempotencyKey)) {
|
|
432
|
+
if (!(await hasCreditLedgerEntry(deps.db, accountId, holdIdempotencyKey))) {
|
|
357
433
|
return;
|
|
358
434
|
}
|
|
359
435
|
await applyCreditLedgerEntry(deps.db, {
|
|
@@ -367,7 +443,11 @@ async function releaseDisputedCredits(deps: ApiRouteDeps, stripe: Stripe, event:
|
|
|
367
443
|
});
|
|
368
444
|
}
|
|
369
445
|
|
|
370
|
-
async function mirrorCustomer(
|
|
446
|
+
async function mirrorCustomer(
|
|
447
|
+
deps: ApiRouteDeps,
|
|
448
|
+
event: Stripe.Event,
|
|
449
|
+
customer: Stripe.Customer,
|
|
450
|
+
): Promise<void> {
|
|
371
451
|
const accountId = customer.metadata?.opengeni_account_id;
|
|
372
452
|
if (!accountId || customer.deleted) {
|
|
373
453
|
return;
|
|
@@ -380,7 +460,11 @@ async function mirrorCustomer(deps: ApiRouteDeps, event: Stripe.Event, customer:
|
|
|
380
460
|
});
|
|
381
461
|
}
|
|
382
462
|
|
|
383
|
-
function recordCreditMicrosMetric(
|
|
463
|
+
function recordCreditMicrosMetric(
|
|
464
|
+
deps: ApiRouteDeps,
|
|
465
|
+
kind: "grant" | "topup" | "refund",
|
|
466
|
+
amountMicros: number,
|
|
467
|
+
): void {
|
|
384
468
|
if (amountMicros <= 0) {
|
|
385
469
|
return;
|
|
386
470
|
}
|
|
@@ -392,7 +476,10 @@ function recordCreditMicrosMetric(deps: ApiRouteDeps, kind: "grant" | "topup" |
|
|
|
392
476
|
});
|
|
393
477
|
}
|
|
394
478
|
|
|
395
|
-
async function metadataForRefund(
|
|
479
|
+
async function metadataForRefund(
|
|
480
|
+
stripe: Stripe,
|
|
481
|
+
refund: Stripe.Refund,
|
|
482
|
+
): Promise<Stripe.Metadata | null> {
|
|
396
483
|
if (Object.keys(refund.metadata ?? {}).length > 0) {
|
|
397
484
|
return refund.metadata;
|
|
398
485
|
}
|
|
@@ -400,11 +487,17 @@ async function metadataForRefund(stripe: Stripe, refund: Stripe.Refund): Promise
|
|
|
400
487
|
return paymentIntent ? (await stripe.paymentIntents.retrieve(paymentIntent)).metadata : null;
|
|
401
488
|
}
|
|
402
489
|
|
|
403
|
-
async function metadataForDispute(
|
|
490
|
+
async function metadataForDispute(
|
|
491
|
+
stripe: Stripe,
|
|
492
|
+
dispute: Stripe.Dispute,
|
|
493
|
+
): Promise<Stripe.Metadata | null> {
|
|
404
494
|
if (Object.keys(dispute.metadata ?? {}).length > 0) {
|
|
405
495
|
return dispute.metadata;
|
|
406
496
|
}
|
|
407
|
-
const paymentIntent = paymentIntentId(
|
|
497
|
+
const paymentIntent = paymentIntentId(
|
|
498
|
+
(dispute as unknown as { payment_intent?: string | Stripe.PaymentIntent | null })
|
|
499
|
+
.payment_intent,
|
|
500
|
+
);
|
|
408
501
|
if (paymentIntent) {
|
|
409
502
|
return (await stripe.paymentIntents.retrieve(paymentIntent)).metadata;
|
|
410
503
|
}
|
|
@@ -414,10 +507,15 @@ async function metadataForDispute(stripe: Stripe, dispute: Stripe.Dispute): Prom
|
|
|
414
507
|
}
|
|
415
508
|
const charge = await stripe.charges.retrieve(chargeId);
|
|
416
509
|
const chargePaymentIntent = paymentIntentId(charge.payment_intent);
|
|
417
|
-
return chargePaymentIntent
|
|
510
|
+
return chargePaymentIntent
|
|
511
|
+
? (await stripe.paymentIntents.retrieve(chargePaymentIntent)).metadata
|
|
512
|
+
: charge.metadata;
|
|
418
513
|
}
|
|
419
514
|
|
|
420
|
-
function creditMetadata(
|
|
515
|
+
function creditMetadata(
|
|
516
|
+
metadata: Stripe.Metadata | null | undefined,
|
|
517
|
+
label: string,
|
|
518
|
+
): {
|
|
421
519
|
accountId: string;
|
|
422
520
|
amountMicros: number;
|
|
423
521
|
idempotencyKey: string;
|
|
@@ -434,12 +532,19 @@ function creditMetadata(metadata: Stripe.Metadata | null | undefined, label: str
|
|
|
434
532
|
accountId,
|
|
435
533
|
amountMicros,
|
|
436
534
|
idempotencyKey,
|
|
437
|
-
...(metadata?.opengeni_credit_amount_usd
|
|
535
|
+
...(metadata?.opengeni_credit_amount_usd
|
|
536
|
+
? { amountUsd: metadata.opengeni_credit_amount_usd }
|
|
537
|
+
: {}),
|
|
438
538
|
...(metadata?.opengeni_package_id ? { packageId: metadata.opengeni_package_id } : {}),
|
|
439
539
|
};
|
|
440
540
|
}
|
|
441
541
|
|
|
442
|
-
async function getOrCreateStripeCustomer(
|
|
542
|
+
async function getOrCreateStripeCustomer(
|
|
543
|
+
deps: ApiRouteDeps,
|
|
544
|
+
stripe: Stripe,
|
|
545
|
+
context: AccessContext,
|
|
546
|
+
accountId: string,
|
|
547
|
+
): Promise<string> {
|
|
443
548
|
const provider = stripeCustomerProvider(deps);
|
|
444
549
|
const existing = await getBillingCustomer(deps.db, accountId, provider);
|
|
445
550
|
if (existing) {
|
|
@@ -465,22 +570,32 @@ async function getOrCreateStripeCustomer(deps: ApiRouteDeps, stripe: Stripe, con
|
|
|
465
570
|
return customer.id;
|
|
466
571
|
}
|
|
467
572
|
|
|
468
|
-
export function stripeCustomerProvider(
|
|
573
|
+
export function stripeCustomerProvider(
|
|
574
|
+
input: ApiRouteDeps | Stripe.Event,
|
|
575
|
+
): "stripe:live" | "stripe:test" {
|
|
469
576
|
if ("livemode" in input) {
|
|
470
577
|
return input.livemode ? "stripe:live" : "stripe:test";
|
|
471
578
|
}
|
|
472
|
-
return input.settings.stripeSecretKey?.startsWith("sk_live_") ||
|
|
579
|
+
return input.settings.stripeSecretKey?.startsWith("sk_live_") ||
|
|
580
|
+
input.settings.stripeSecretKey?.startsWith("rk_live_")
|
|
473
581
|
? "stripe:live"
|
|
474
582
|
: "stripe:test";
|
|
475
583
|
}
|
|
476
584
|
|
|
477
|
-
function requireSelectedAccount(
|
|
585
|
+
function requireSelectedAccount(
|
|
586
|
+
context: AccessContext,
|
|
587
|
+
requested: string | undefined,
|
|
588
|
+
permission: Permission,
|
|
589
|
+
): string {
|
|
478
590
|
const accountId = requested ?? context.defaultAccountId ?? undefined;
|
|
479
591
|
if (!accountId) {
|
|
480
592
|
throw new HTTPException(409, { message: "account selection is required" });
|
|
481
593
|
}
|
|
482
594
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
483
|
-
if (
|
|
595
|
+
if (
|
|
596
|
+
!grant ||
|
|
597
|
+
(!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
|
|
598
|
+
) {
|
|
484
599
|
throw new HTTPException(403, { message: `missing permission: ${permission}` });
|
|
485
600
|
}
|
|
486
601
|
return accountId;
|
|
@@ -23,30 +23,39 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
23
23
|
app.get("/v1/workspaces/:workspaceId/capabilities", async (c) => {
|
|
24
24
|
const workspaceId = c.req.param("workspaceId");
|
|
25
25
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
26
|
-
return c.json(
|
|
26
|
+
return c.json(
|
|
27
|
+
CapabilityCatalogResponse.parse(await buildCapabilityCatalog({ db, workspaceId, settings })),
|
|
28
|
+
);
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
app.post("/v1/workspaces/:workspaceId/capabilities", async (c) => {
|
|
30
32
|
const workspaceId = c.req.param("workspaceId");
|
|
31
33
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
32
34
|
const payload = CreateCapabilityCatalogItemRequest.parse(await c.req.json());
|
|
33
|
-
return c.json(
|
|
35
|
+
return c.json(
|
|
36
|
+
await createCatalogItem({ db, accountId: grant.accountId, workspaceId, payload }),
|
|
37
|
+
201,
|
|
38
|
+
);
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
app.get("/v1/workspaces/:workspaceId/capabilities/discovery/mcp-registry", async (c) => {
|
|
37
42
|
const workspaceId = c.req.param("workspaceId");
|
|
38
43
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
39
44
|
const query = c.req.query("query");
|
|
40
|
-
const options: { query?: string; limit?: number } = {
|
|
45
|
+
const options: { query?: string; limit?: number } = {
|
|
46
|
+
limit: boundedLimit(c.req.query("limit")),
|
|
47
|
+
};
|
|
41
48
|
if (query) {
|
|
42
49
|
options.query = query;
|
|
43
50
|
}
|
|
44
51
|
const items = await discoverMcpRegistryCapabilities(options);
|
|
45
|
-
return c.json(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
return c.json(
|
|
53
|
+
DiscoverMcpCapabilitiesResponse.parse({
|
|
54
|
+
items,
|
|
55
|
+
source: "official_mcp_registry",
|
|
56
|
+
sourceUrl: officialMcpRegistryUrl,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
50
59
|
});
|
|
51
60
|
|
|
52
61
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
|
|
@@ -83,7 +83,7 @@ export function catalogAssetKeyFromPath(pathname: string): string | null {
|
|
|
83
83
|
function contentTypeForKey(key: string): string | null {
|
|
84
84
|
const match = /\.([a-zA-Z0-9]+)$/.exec(key);
|
|
85
85
|
const ext = match?.[1]?.toLowerCase();
|
|
86
|
-
return ext ? CONTENT_TYPE_BY_EXTENSION[ext] ?? null : null;
|
|
86
|
+
return ext ? (CONTENT_TYPE_BY_EXTENSION[ext] ?? null) : null;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** Digest-keyed filenames (`{domain}/{digest24}.{ext}`) make the basename itself a stable ETag. */
|
|
@@ -101,5 +101,8 @@ function ifNoneMatchSatisfied(header: string | undefined, etag: string): boolean
|
|
|
101
101
|
if (header.trim() === "*") {
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
|
-
return header
|
|
104
|
+
return header
|
|
105
|
+
.split(",")
|
|
106
|
+
.map((value) => value.trim())
|
|
107
|
+
.includes(etag);
|
|
105
108
|
}
|