@opengeni/api-router 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-YY6OAEL6.js → chunk-DO2G3JSB.js} +5333 -2205
  3. package/dist/chunk-DO2G3JSB.js.map +1 -0
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +297 -54
  6. package/dist/index.js.map +1 -1
  7. package/package.json +21 -21
  8. package/src/app.ts +415 -147
  9. package/src/auth/managed-auth.ts +32 -16
  10. package/src/http/auth.ts +8 -1
  11. package/src/http/common.ts +6 -2
  12. package/src/http/sse.ts +27 -6
  13. package/src/index.ts +196 -74
  14. package/src/integrations/oauth-client.ts +592 -131
  15. package/src/integrations/provider-domain.ts +4 -1
  16. package/src/mcp/documents.ts +173 -94
  17. package/src/mcp/server.ts +1517 -692
  18. package/src/mcp/session-view.ts +8 -2
  19. package/src/mcp/toolspace.ts +175 -84
  20. package/src/observability.ts +7 -1
  21. package/src/routes/api-keys.ts +39 -23
  22. package/src/routes/billing.ts +180 -65
  23. package/src/routes/capabilities.ts +17 -8
  24. package/src/routes/catalog-assets.ts +5 -2
  25. package/src/routes/codex.ts +244 -63
  26. package/src/routes/connections.ts +72 -34
  27. package/src/routes/documents.ts +242 -92
  28. package/src/routes/enrollments.ts +100 -70
  29. package/src/routes/environments.ts +205 -136
  30. package/src/routes/files.ts +164 -39
  31. package/src/routes/github.ts +123 -50
  32. package/src/routes/install.ts +9 -2
  33. package/src/routes/machines.ts +9 -8
  34. package/src/routes/packs.ts +141 -89
  35. package/src/routes/rigs.ts +189 -0
  36. package/src/routes/scheduled-tasks.ts +51 -9
  37. package/src/routes/sessions.ts +839 -329
  38. package/src/routes/social.ts +50 -38
  39. package/src/routes/workspace-capture.ts +238 -0
  40. package/src/routes/workspaces.ts +159 -13
  41. package/src/sandbox/access.ts +11 -3
  42. package/src/sandbox/auth-callout.ts +5 -1
  43. package/src/sandbox/channel-a.ts +104 -27
  44. package/src/sandbox/enrollment.ts +13 -3
  45. package/src/sandbox/machines.ts +68 -59
  46. package/src/sandbox/metrics-ingestion.ts +238 -17
  47. package/src/sandbox/viewer.ts +172 -46
  48. package/dist/chunk-YY6OAEL6.js.map +0 -1
@@ -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({ mode: deps.settings.billingMode, balance: await getBillingBalance(deps.db, accountId) });
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 (workspaceId && !context.workspaceGrants.some((grant) => grant.accountId === accountId && grant.workspaceId === workspaceId)) {
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({ accountId, mode: deps.settings.entitlementsMode, entitlements: configuredEntitlements(deps.settings) });
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, { message: parsed.error.issues[0]?.message ?? "invalid checkout request" });
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(stripeCheckoutSessionCreateParams({
73
- accountId,
74
- customerId,
75
- amountCents,
76
- amountMicros,
77
- creditsProductId: deps.settings.stripeCreditsProductId,
78
- publicBaseUrl: deps.settings.publicBaseUrl,
79
- successUrl: body.successUrl,
80
- cancelUrl: body.cancelUrl,
81
- idempotencyKey,
82
- }), { idempotencyKey });
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(CreateCheckoutResponse.parse({
87
- checkoutSessionId: session.id,
88
- url: session.url,
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(payload, signature, deps.settings.stripeWebhookSecret!);
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, { message: error instanceof Error ? error.message : "invalid stripe signature" });
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, { message: error instanceof Error ? error.message : String(error) });
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(input.publicBaseUrl, input.successUrl, "/billing?checkout=success", "successUrl");
140
- const cancelUrl = checkoutReturnUrl(input.publicBaseUrl, input.cancelUrl, "/billing?checkout=cancelled", "cancelUrl");
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
- quantity: 1,
154
- price_data: {
155
- currency: "usd",
156
- unit_amount: input.amountCents,
157
- ...(input.creditsProductId
158
- ? { product: input.creditsProductId }
159
- : {
160
- product_data: {
161
- name: "OpenGeni credits",
162
- metadata: {
163
- app: "opengeni",
164
- billing_model: "prepaid_credits",
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(publicBaseUrl: string | undefined, candidate: string | undefined, fallbackPath: string, field: string): string {
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, { message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout" });
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(deps: ApiRouteDeps, stripe: Stripe, event: Stripe.Event): Promise<void> {
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(deps: ApiRouteDeps, event: Stripe.Event): Promise<void> {
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: typeof session.payment_intent === "string" ? session.payment_intent : session.payment_intent?.id ?? null,
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(deps: ApiRouteDeps, stripe: Stripe, event: Stripe.Event): Promise<void> {
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(deps: ApiRouteDeps, stripe: Stripe, event: Stripe.Event): Promise<void> {
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(deps: ApiRouteDeps, stripe: Stripe, refund: Stripe.Refund): Promise<void> {
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(deps: ApiRouteDeps, stripe: Stripe, event: Stripe.Event): Promise<void> {
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(deps: ApiRouteDeps, stripe: Stripe, event: Stripe.Event): Promise<void> {
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(deps: ApiRouteDeps, event: Stripe.Event, customer: Stripe.Customer): Promise<void> {
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(deps: ApiRouteDeps, kind: "grant" | "topup" | "refund", amountMicros: number): void {
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(stripe: Stripe, refund: Stripe.Refund): Promise<Stripe.Metadata | null> {
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(stripe: Stripe, dispute: Stripe.Dispute): Promise<Stripe.Metadata | null> {
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((dispute as unknown as { payment_intent?: string | Stripe.PaymentIntent | null }).payment_intent);
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 ? (await stripe.paymentIntents.retrieve(chargePaymentIntent)).metadata : charge.metadata;
510
+ return chargePaymentIntent
511
+ ? (await stripe.paymentIntents.retrieve(chargePaymentIntent)).metadata
512
+ : charge.metadata;
418
513
  }
419
514
 
420
- function creditMetadata(metadata: Stripe.Metadata | null | undefined, label: string): {
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 ? { amountUsd: 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(deps: ApiRouteDeps, stripe: Stripe, context: AccessContext, accountId: string): Promise<string> {
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(input: ApiRouteDeps | Stripe.Event): "stripe:live" | "stripe:test" {
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_") || input.settings.stripeSecretKey?.startsWith("rk_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(context: AccessContext, requested: string | undefined, permission: Permission): string {
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 (!grant || (!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))) {
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(CapabilityCatalogResponse.parse(await buildCapabilityCatalog({ db, workspaceId, settings })));
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(await createCatalogItem({ db, accountId: grant.accountId, workspaceId, payload }), 201);
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 } = { limit: boundedLimit(c.req.query("limit")) };
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(DiscoverMcpCapabilitiesResponse.parse({
46
- items,
47
- source: "official_mcp_registry",
48
- sourceUrl: officialMcpRegistryUrl,
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.split(",").map((value) => value.trim()).includes(etag);
104
+ return header
105
+ .split(",")
106
+ .map((value) => value.trim())
107
+ .includes(etag);
105
108
  }