@opengeni/api-router 0.5.3 → 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-3HIA43CC.js → chunk-DO2G3JSB.js} +5184 -2195
  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 +20 -20
  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 +403 -120
  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 +71 -33
  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-3HIA43CC.js.map +0 -1
@@ -208,7 +208,10 @@ function buildTruncationEvent(
208
208
  * head and a tail of events and drop the middle behind a marker. `events` is
209
209
  * assumed oldest-first (as `listSessionEvents` returns).
210
210
  */
211
- export function capEventPage(events: SessionEvent[], config: EventCapConfig = DEFAULT_EVENT_CAP): CappedEventPage {
211
+ export function capEventPage(
212
+ events: SessionEvent[],
213
+ config: EventCapConfig = DEFAULT_EVENT_CAP,
214
+ ): CappedEventPage {
212
215
  const realLast = events[events.length - 1];
213
216
  const nextAfter = realLast ? realLast.sequence : null;
214
217
 
@@ -274,7 +277,10 @@ export function capSessionDetail<T extends { metadata?: unknown; initialMessage?
274
277
  }
275
278
  }
276
279
  if (typeof session.initialMessage === "string" && session.initialMessage.length > perFieldChars) {
277
- (out as { initialMessage?: unknown }).initialMessage = clampString(session.initialMessage, perFieldChars);
280
+ (out as { initialMessage?: unknown }).initialMessage = clampString(
281
+ session.initialMessage,
282
+ perFieldChars,
283
+ );
278
284
  changed = true;
279
285
  }
280
286
  return changed ? out : session;
@@ -4,7 +4,11 @@ import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/tran
4
4
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { environmentsEncryptionKeyBytes, type McpServerConfig } from "@opengeni/config";
6
6
  import { prefixedMcpToolName, type AccessGrant, type ToolRef } from "@opengeni/contracts";
7
- import { hasPermission, settingsWithEnabledCapabilityMcpServers, type ApiRouteDeps } from "@opengeni/core";
7
+ import {
8
+ hasPermission,
9
+ settingsWithEnabledCapabilityMcpServers,
10
+ type ApiRouteDeps,
11
+ } from "@opengeni/core";
8
12
  import {
9
13
  buildConnectionTokenResolver,
10
14
  listSessionMcpServerMetadata,
@@ -45,8 +49,10 @@ type McpTool = {
45
49
 
46
50
  const APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
47
51
  const TOOLSPACE_AUTH_NEEDED_ERROR_CODE = -32001;
48
- const TOOLSPACE_AUTH_NEEDED_MESSAGE = "Authentication required - a connection link was posted to the session.";
49
- const TOOLSPACE_NO_ACTIVE_TURN_MESSAGE = "no active turn - toolspace calls require an in-flight turn";
52
+ const TOOLSPACE_AUTH_NEEDED_MESSAGE =
53
+ "Authentication required - a connection link was posted to the session.";
54
+ const TOOLSPACE_NO_ACTIVE_TURN_MESSAGE =
55
+ "no active turn - toolspace calls require an in-flight turn";
50
56
  // First-party OpenGeni MCP proxies (files/docs) route back through the same
51
57
  // /mcp mount. They are excluded from the toolspace surface by construction so a
52
58
  // toolspace principal can never re-enter /mcp as a first-party caller, even if
@@ -68,9 +74,11 @@ type ToolListingEntry = {
68
74
  };
69
75
 
70
76
  export function isToolspaceGrant(settings: ApiRouteDeps["settings"], grant: AccessGrant): boolean {
71
- return settings.toolspaceEnabled
72
- && hasPermission(grant.permissions, "toolspace:call")
73
- && typeof grant.metadata?.sessionId === "string";
77
+ return (
78
+ settings.toolspaceEnabled &&
79
+ hasPermission(grant.permissions, "toolspace:call") &&
80
+ typeof grant.metadata?.sessionId === "string"
81
+ );
74
82
  }
75
83
 
76
84
  export async function prepareToolspaceMcpSurface(input: {
@@ -83,7 +91,10 @@ export async function prepareToolspaceMcpSurface(input: {
83
91
  }
84
92
  const sessionId = grant.metadata!.sessionId as string;
85
93
  const session = await requireSession(deps.db, grant.workspaceId, sessionId);
86
- const selectedIds = selectedMcpServerIds(session.tools, session.mcpServers.map((server) => server.id));
94
+ const selectedIds = selectedMcpServerIds(
95
+ session.tools,
96
+ session.mcpServers.map((server) => server.id),
97
+ );
87
98
  // Proxyable ids: everything selected except the first-party OpenGeni tool
88
99
  // server and the first-party MCP proxies, both of which would re-enter /mcp.
89
100
  const proxyableIds = [...selectedIds].filter((id) => toolspaceCanProxyServerId(id));
@@ -96,7 +107,8 @@ export async function prepareToolspaceMcpSurface(input: {
96
107
  // request, and only when we actually need it (a cache-miss listing or a real
97
108
  // tools/call), so a cache-hit request does no registry work.
98
109
  let registryPromise: Promise<Map<string, McpServerConfig>> | null = null;
99
- const getRegistry = () => (registryPromise ??= buildToolspaceRegistry(deps, grant.workspaceId, sessionId));
110
+ const getRegistry = () =>
111
+ (registryPromise ??= buildToolspaceRegistry(deps, grant.workspaceId, sessionId));
100
112
 
101
113
  const listing = await resolveToolListing({
102
114
  deps,
@@ -106,7 +118,9 @@ export async function prepareToolspaceMcpSurface(input: {
106
118
  activeTurnId: session.activeTurnId ?? null,
107
119
  getRegistry,
108
120
  });
109
- const tools = listing.map((entry) => toolspaceToolFor({ deps, grant, sessionId, entry, getRegistry }));
121
+ const tools = listing.map((entry) =>
122
+ toolspaceToolFor({ deps, grant, sessionId, entry, getRegistry }),
123
+ );
110
124
 
111
125
  return {
112
126
  sessionId,
@@ -127,8 +141,17 @@ async function buildToolspaceRegistry(
127
141
  workspaceId: string,
128
142
  sessionId: string,
129
143
  ): Promise<Map<string, McpServerConfig>> {
130
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(deps.db, workspaceId, deps.settings);
131
- const withSessionServers = await settingsWithSessionMcpServersForToolspace(deps, workspaceId, sessionId, runtimeSettings);
144
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
145
+ deps.db,
146
+ workspaceId,
147
+ deps.settings,
148
+ );
149
+ const withSessionServers = await settingsWithSessionMcpServersForToolspace(
150
+ deps,
151
+ workspaceId,
152
+ sessionId,
153
+ runtimeSettings,
154
+ );
132
155
  return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
133
156
  }
134
157
 
@@ -161,12 +184,16 @@ async function resolveToolListing(input: {
161
184
  if (!config || !toolspaceCanProxyServer(config)) {
162
185
  continue;
163
186
  }
164
- const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(() => null);
187
+ const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(
188
+ () => null,
189
+ );
165
190
  if (!connection) {
166
191
  continue;
167
192
  }
168
193
  try {
169
- const listed = await connection.client.listTools(undefined, toolspaceRequestOptions(config)).catch(() => ({ tools: [] }));
194
+ const listed = await connection.client
195
+ .listTools(undefined, toolspaceRequestOptions(config))
196
+ .catch(() => ({ tools: [] }));
170
197
  for (const tool of listed.tools as McpTool[]) {
171
198
  if (!tool?.name || !allowedByConfig(config, tool.name)) {
172
199
  continue;
@@ -254,7 +281,9 @@ async function settingsWithSessionMcpServersForToolspace(
254
281
  ...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
255
282
  ...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
256
283
  cacheToolsList: server.cacheToolsList ?? false,
257
- ...(server.requireApproval !== undefined ? { requireApproval: server.requireApproval } : {}),
284
+ ...(server.requireApproval !== undefined
285
+ ? { requireApproval: server.requireApproval }
286
+ : {}),
258
287
  headers: server.headers,
259
288
  })),
260
289
  ],
@@ -270,7 +299,10 @@ async function connectToolspaceServer(input: {
270
299
  const baseFetch: FetchLike = input.config.connectionRef
271
300
  ? connectionBrokerFetch(globalThis.fetch, input)
272
301
  : globalThis.fetch;
273
- const client = new Client({ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" }, { capabilities: {} });
302
+ const client = new Client(
303
+ { name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" },
304
+ { capabilities: {} },
305
+ );
274
306
  const transport = new StreamableHTTPClientTransport(new URL(input.config.url), {
275
307
  ...(baseFetch !== globalThis.fetch ? { fetch: baseFetch } : {}),
276
308
  requestInit: {
@@ -314,7 +346,9 @@ function toolspaceToolFor(input: {
314
346
  return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
315
347
  }
316
348
  if (reservation.status === "budget_exhausted") {
317
- return mcpError(`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`);
349
+ return mcpError(
350
+ `toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`,
351
+ );
318
352
  }
319
353
  const turnId = reservation.turnId;
320
354
  // Dial only the ONE server this tool belongs to, from the freshly-built
@@ -328,41 +362,47 @@ function toolspaceToolFor(input: {
328
362
  if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
329
363
  return mcpError(APPROVAL_REQUIRED_MESSAGE);
330
364
  }
331
- const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(() => null);
365
+ const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(
366
+ () => null,
367
+ );
332
368
  if (!connection) {
333
369
  return mcpError(`upstream tool failed: ${name}`);
334
370
  }
335
371
  try {
336
372
  const callId = crypto.randomUUID();
337
- await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [{
338
- type: "agent.toolCall.created",
339
- turnId,
340
- producerId: grant.subjectId,
341
- payload: {
342
- id: callId,
343
- name,
344
- arguments: args,
345
- origin: "toolspace",
346
- subjectId: grant.subjectId,
347
- raw: {
348
- type: "toolspace_call",
349
- serverId,
350
- toolName: tool.name,
373
+ await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
374
+ {
375
+ type: "agent.toolCall.created",
376
+ turnId,
377
+ producerId: grant.subjectId,
378
+ payload: {
379
+ id: callId,
380
+ name,
381
+ arguments: args,
382
+ origin: "toolspace",
383
+ subjectId: grant.subjectId,
384
+ raw: {
385
+ type: "toolspace_call",
386
+ serverId,
387
+ toolName: tool.name,
388
+ },
351
389
  },
352
390
  },
353
- }]);
391
+ ]);
354
392
  const output = await callRemoteTool(deps, connection, tool.name, args);
355
- await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [{
356
- type: "agent.toolCall.output",
357
- turnId,
358
- producerId: grant.subjectId,
359
- payload: {
360
- id: callId,
361
- output,
362
- origin: "toolspace",
363
- subjectId: grant.subjectId,
393
+ await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
394
+ {
395
+ type: "agent.toolCall.output",
396
+ turnId,
397
+ producerId: grant.subjectId,
398
+ payload: {
399
+ id: callId,
400
+ output,
401
+ origin: "toolspace",
402
+ subjectId: grant.subjectId,
403
+ },
364
404
  },
365
- }]);
405
+ ]);
366
406
  return output;
367
407
  } finally {
368
408
  await connection.close();
@@ -378,10 +418,14 @@ async function callRemoteTool(
378
418
  args: Record<string, unknown>,
379
419
  ): Promise<ToolspaceCallResult> {
380
420
  try {
381
- return await server.client.callTool({
382
- name: toolName,
383
- arguments: args,
384
- }, undefined, toolspaceRequestOptions(server.config)) as ToolspaceCallResult;
421
+ return (await server.client.callTool(
422
+ {
423
+ name: toolName,
424
+ arguments: args,
425
+ },
426
+ undefined,
427
+ toolspaceRequestOptions(server.config),
428
+ )) as ToolspaceCallResult;
385
429
  } catch (error) {
386
430
  if (isToolspaceAuthNeededError(error)) {
387
431
  return mcpError(TOOLSPACE_AUTH_NEEDED_MESSAGE);
@@ -403,7 +447,11 @@ type ToolspaceReservation =
403
447
  | { status: "no_active_turn" }
404
448
  | { status: "budget_exhausted" };
405
449
 
406
- async function reserveActiveTurnCall(deps: ApiRouteDeps, workspaceId: string, sessionId: string): Promise<ToolspaceReservation> {
450
+ async function reserveActiveTurnCall(
451
+ deps: ApiRouteDeps,
452
+ workspaceId: string,
453
+ sessionId: string,
454
+ ): Promise<ToolspaceReservation> {
407
455
  const session = await requireSession(deps.db, workspaceId, sessionId);
408
456
  if (!session.activeTurnId) {
409
457
  return { status: "no_active_turn" };
@@ -458,7 +506,10 @@ function allowedByConfig(config: McpServerConfig, toolName: string): boolean {
458
506
  return !config.allowedTools || config.allowedTools.includes(toolName);
459
507
  }
460
508
 
461
- function mcpToolRequiresApproval(policy: McpServerConfig["requireApproval"], unprefixedName: string): boolean {
509
+ function mcpToolRequiresApproval(
510
+ policy: McpServerConfig["requireApproval"],
511
+ unprefixedName: string,
512
+ ): boolean {
462
513
  if (policy === true) {
463
514
  return true;
464
515
  }
@@ -472,7 +523,10 @@ function mcpError(message: string): ToolspaceCallResult {
472
523
  };
473
524
  }
474
525
 
475
- function toolspaceRequestOptions(config: McpServerConfig): { timeout?: number; maxTotalTimeout?: number } {
526
+ function toolspaceRequestOptions(config: McpServerConfig): {
527
+ timeout?: number;
528
+ maxTotalTimeout?: number;
529
+ } {
476
530
  return config.timeoutMs ? { timeout: config.timeoutMs, maxTotalTimeout: config.timeoutMs } : {};
477
531
  }
478
532
 
@@ -509,7 +563,10 @@ function connectionBrokerFetch(
509
563
  if (first.status === "auth_needed") {
510
564
  return await authNeededFetchResponse(input, request, first);
511
565
  }
512
- const response = await baseFetch(fetchInputForAttempt(requestInput), withConnectionHeaders(requestInput, init, first.headers));
566
+ const response = await baseFetch(
567
+ fetchInputForAttempt(requestInput),
568
+ withConnectionHeaders(requestInput, init, first.headers),
569
+ );
513
570
  if (response.status === 401) {
514
571
  const refreshed = await resolveCredential({
515
572
  workspaceId: input.grant.workspaceId,
@@ -522,10 +579,17 @@ function connectionBrokerFetch(
522
579
  if (refreshed.status === "auth_needed") {
523
580
  return await authNeededFetchResponse(input, request, refreshed);
524
581
  }
525
- return await baseFetch(fetchInputForAttempt(requestInput), withConnectionHeaders(requestInput, init, refreshed.headers));
582
+ return await baseFetch(
583
+ fetchInputForAttempt(requestInput),
584
+ withConnectionHeaders(requestInput, init, refreshed.headers),
585
+ );
526
586
  }
527
587
  if (response.status === 403) {
528
- return await authNeededFetchResponse(input, request, authNeededFromStatus(input.config, first, "insufficient_scope"));
588
+ return await authNeededFetchResponse(
589
+ input,
590
+ request,
591
+ authNeededFromStatus(input.config, first, "insufficient_scope"),
592
+ );
529
593
  }
530
594
  return response;
531
595
  };
@@ -557,33 +621,44 @@ async function authNeededFetchResponse(
557
621
  request: McpRequestInfo,
558
622
  auth: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
559
623
  ): Promise<Response> {
560
- await appendAndPublishEvents(input.deps.db, input.deps.bus, input.grant.workspaceId, input.sessionId, [{
561
- type: "tool.auth_needed",
562
- producerId: input.grant.subjectId,
563
- payload: {
564
- serverId: input.config.id,
565
- toolName: request.toolName ?? null,
566
- providerDomain: auth.providerDomain,
567
- reason: auth.reason,
568
- ...(auth.connectionId ? { connectionId: auth.connectionId } : {}),
569
- ...(auth.scopes ? { scopes: auth.scopes } : {}),
570
- ...(auth.resource ? { resource: auth.resource } : {}),
571
- ...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
572
- subjectId: input.grant.subjectId,
573
- },
574
- }]).catch(() => undefined);
624
+ await appendAndPublishEvents(
625
+ input.deps.db,
626
+ input.deps.bus,
627
+ input.grant.workspaceId,
628
+ input.sessionId,
629
+ [
630
+ {
631
+ type: "tool.auth_needed",
632
+ producerId: input.grant.subjectId,
633
+ payload: {
634
+ serverId: input.config.id,
635
+ toolName: request.toolName ?? null,
636
+ providerDomain: auth.providerDomain,
637
+ reason: auth.reason,
638
+ ...(auth.connectionId ? { connectionId: auth.connectionId } : {}),
639
+ ...(auth.scopes ? { scopes: auth.scopes } : {}),
640
+ ...(auth.resource ? { resource: auth.resource } : {}),
641
+ ...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
642
+ subjectId: input.grant.subjectId,
643
+ },
644
+ },
645
+ ],
646
+ ).catch(() => undefined);
575
647
  if (request.method === "tools/call") {
576
- return new Response(JSON.stringify({
577
- jsonrpc: "2.0",
578
- id: request.id ?? null,
579
- error: {
580
- code: TOOLSPACE_AUTH_NEEDED_ERROR_CODE,
581
- message: TOOLSPACE_AUTH_NEEDED_MESSAGE,
648
+ return new Response(
649
+ JSON.stringify({
650
+ jsonrpc: "2.0",
651
+ id: request.id ?? null,
652
+ error: {
653
+ code: TOOLSPACE_AUTH_NEEDED_ERROR_CODE,
654
+ message: TOOLSPACE_AUTH_NEEDED_MESSAGE,
655
+ },
656
+ }),
657
+ {
658
+ status: 200,
659
+ headers: { "content-type": "application/json" },
582
660
  },
583
- }), {
584
- status: 200,
585
- headers: { "content-type": "application/json" },
586
- });
661
+ );
587
662
  }
588
663
  return new Response("Authentication required for MCP server connection", { status: 401 });
589
664
  }
@@ -594,10 +669,20 @@ async function mcpRequestInfo(_input: string | URL, init?: RequestInit): Promise
594
669
  return {};
595
670
  }
596
671
  try {
597
- const parsed = JSON.parse(body) as { id?: unknown; method?: unknown; params?: { name?: unknown } };
672
+ const parsed = JSON.parse(body) as {
673
+ id?: unknown;
674
+ method?: unknown;
675
+ params?: { name?: unknown };
676
+ };
598
677
  const method = typeof parsed.method === "string" ? parsed.method : undefined;
599
- const id = typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null ? parsed.id : undefined;
600
- const toolName = method === "tools/call" && typeof parsed.params?.name === "string" ? parsed.params.name : undefined;
678
+ const id =
679
+ typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null
680
+ ? parsed.id
681
+ : undefined;
682
+ const toolName =
683
+ method === "tools/call" && typeof parsed.params?.name === "string"
684
+ ? parsed.params.name
685
+ : undefined;
601
686
  return {
602
687
  ...(method ? { method } : {}),
603
688
  ...(id !== undefined ? { id } : {}),
@@ -608,7 +693,11 @@ async function mcpRequestInfo(_input: string | URL, init?: RequestInit): Promise
608
693
  }
609
694
  }
610
695
 
611
- function withConnectionHeaders(_input: string | URL, init: RequestInit | undefined, authHeaders: Record<string, string>): RequestInit {
696
+ function withConnectionHeaders(
697
+ _input: string | URL,
698
+ init: RequestInit | undefined,
699
+ authHeaders: Record<string, string>,
700
+ ): RequestInit {
612
701
  const headers = new Headers(init?.headers);
613
702
  for (const [name, value] of Object.entries(authHeaders)) {
614
703
  headers.set(name, value);
@@ -621,7 +710,9 @@ function fetchInputForAttempt(input: string | URL): string | URL {
621
710
  }
622
711
 
623
712
  function isToolspaceAuthNeededError(error: unknown): boolean {
624
- return error instanceof Error
625
- && (((error as { code?: unknown }).code === TOOLSPACE_AUTH_NEEDED_ERROR_CODE)
626
- || error.message.includes(TOOLSPACE_AUTH_NEEDED_MESSAGE));
713
+ return (
714
+ error instanceof Error &&
715
+ ((error as { code?: unknown }).code === TOOLSPACE_AUTH_NEEDED_ERROR_CODE ||
716
+ error.message.includes(TOOLSPACE_AUTH_NEEDED_MESSAGE))
717
+ );
627
718
  }
@@ -20,7 +20,13 @@ function eventAttributes(attributes: Record<string, unknown> | undefined): Attri
20
20
  }
21
21
 
22
22
  function eventAttributeValue(value: unknown): AttributeValue {
23
- if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
23
+ if (
24
+ value === null ||
25
+ value === undefined ||
26
+ typeof value === "string" ||
27
+ typeof value === "number" ||
28
+ typeof value === "boolean"
29
+ ) {
24
30
  return value;
25
31
  }
26
32
  try {
@@ -14,26 +14,36 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
14
14
  return c.json({ apiKeys: await listApiKeys(deps.db, workspaceId) });
15
15
  });
16
16
 
17
- app.post("/v1/workspaces/:workspaceId/api-keys", zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })), async (c) => {
18
- const workspaceId = c.req.param("workspaceId");
19
- const grant = await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
20
- const body = c.req.valid("json");
21
- const permissions: Permission[] = body.permissions.length > 0 ? body.permissions as Permission[] : ["workspace:read"];
22
- ensureDelegablePermissions(grant.permissions, permissions);
23
- await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "api_key:create", quantity: 1 });
24
- const token = generateApiKeyToken();
25
- const prefix = token.slice(0, 14);
26
- const apiKey = await createApiKey(deps.db, {
27
- accountId: grant.accountId,
28
- workspaceId: grant.workspaceId,
29
- name: body.name,
30
- prefix,
31
- keyHash: await sha256Hex(token),
32
- permissions,
33
- expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
34
- });
35
- return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
36
- });
17
+ app.post(
18
+ "/v1/workspaces/:workspaceId/api-keys",
19
+ zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
20
+ async (c) => {
21
+ const workspaceId = c.req.param("workspaceId");
22
+ const grant = await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
23
+ const body = c.req.valid("json");
24
+ const permissions: Permission[] =
25
+ body.permissions.length > 0 ? (body.permissions as Permission[]) : ["workspace:read"];
26
+ ensureDelegablePermissions(grant.permissions, permissions);
27
+ await requireLimit(deps, {
28
+ accountId: grant.accountId,
29
+ workspaceId,
30
+ action: "api_key:create",
31
+ quantity: 1,
32
+ });
33
+ const token = generateApiKeyToken();
34
+ const prefix = token.slice(0, 14);
35
+ const apiKey = await createApiKey(deps.db, {
36
+ accountId: grant.accountId,
37
+ workspaceId: grant.workspaceId,
38
+ name: body.name,
39
+ prefix,
40
+ keyHash: await sha256Hex(token),
41
+ permissions,
42
+ expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
43
+ });
44
+ return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
45
+ },
46
+ );
37
47
 
38
48
  app.delete("/v1/workspaces/:workspaceId/api-keys/:apiKeyId", async (c) => {
39
49
  const workspaceId = c.req.param("workspaceId");
@@ -48,18 +58,24 @@ function ensureDelegablePermissions(grantPermissions: Permission[], requested: P
48
58
  }
49
59
  const missing = requested.filter((permission) => !grantPermissions.includes(permission));
50
60
  if (missing.length > 0) {
51
- throw new HTTPException(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
61
+ throw new HTTPException(403, {
62
+ message: `cannot delegate missing permissions: ${missing.join(", ")}`,
63
+ });
52
64
  }
53
65
  }
54
66
 
55
67
  function generateApiKeyToken(): string {
56
68
  const bytes = new Uint8Array(32);
57
69
  crypto.getRandomValues(bytes);
58
- const secret = Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
70
+ const secret = Array.from(bytes)
71
+ .map((byte) => byte.toString(16).padStart(2, "0"))
72
+ .join("");
59
73
  return `ogk_${secret}`;
60
74
  }
61
75
 
62
76
  async function sha256Hex(value: string): Promise<string> {
63
77
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
64
- return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
78
+ return Array.from(new Uint8Array(digest))
79
+ .map((byte) => byte.toString(16).padStart(2, "0"))
80
+ .join("");
65
81
  }