@opengeni/api-router 0.21.0 → 0.21.7

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.
@@ -0,0 +1,16 @@
1
+ import type { ErrorCode } from "@opengeni/contracts";
2
+ import { HTTPException } from "hono/http-exception";
3
+ type ApiHttpErrorOptions = {
4
+ code: ErrorCode;
5
+ message: string;
6
+ retryable?: boolean;
7
+ details?: Record<string, unknown>;
8
+ };
9
+ /** A public, structured API failure whose message and details are safe for clients. */
10
+ export declare class ApiHttpError extends HTTPException {
11
+ readonly code: ErrorCode;
12
+ readonly retryable: boolean | undefined;
13
+ readonly details: Record<string, unknown> | undefined;
14
+ constructor(status: number, options: ApiHttpErrorOptions);
15
+ }
16
+ export {};
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAppComposition,
3
3
  startSlackInteractionPump
4
- } from "./chunk-5EEC7Q6C.js";
4
+ } from "./chunk-RIFVFI3J.js";
5
5
 
6
6
  // src/index.ts
7
7
  import {
@@ -83,9 +83,13 @@ export declare function browseGoogleDrive(deps: ApiRouteDeps, input: {
83
83
  incompleteSearch: boolean;
84
84
  }>;
85
85
  export declare function saveGoogleDriveSource(deps: ApiRouteDeps, input: {
86
+ accountId: string;
86
87
  workspaceId: string;
87
88
  subjectId: string;
88
89
  connectionId: string;
89
90
  payload: unknown;
91
+ canManageOrganizationDestination: boolean;
92
+ canManageWorkspaceDestination: boolean;
93
+ canManagePersonalDestination: boolean;
90
94
  }): Promise<import("@opengeni/db").ConnectionMetadataWithVerification>;
91
95
  export {};
@@ -9,6 +9,8 @@ type OAuthClientDeps = {
9
9
  db: Database;
10
10
  settings: Settings;
11
11
  observability?: Observability | undefined;
12
+ oauthStartDeadlineMs?: number | undefined;
13
+ oauthCallbackDeadlineMs?: number | undefined;
12
14
  };
13
15
  export type OAuthStartContext = {
14
16
  accountId: string;
@@ -31,6 +33,9 @@ type AuthorizationServerMetadata = {
31
33
  codeChallengeMethodsSupported: string[];
32
34
  raw: Record<string, unknown>;
33
35
  };
36
+ export declare const OAUTH_START_DEADLINE_MS = 15000;
37
+ export declare const OAUTH_CALLBACK_DEADLINE_MS = 30000;
38
+ export type OAuthStartStage = "connection_lookup" | "mcp_challenge" | "protected_resource_metadata" | "authorization_server_metadata" | "client_registration";
34
39
  export declare function startMcpOAuth(deps: OAuthClientDeps, context: OAuthStartContext): Promise<OAuthStartResponse>;
35
40
  export declare function completeMcpOAuthCallback(deps: OAuthClientDeps, input: {
36
41
  code?: string | undefined;
@@ -0,0 +1,4 @@
1
+ import { type WorkspaceStateExportResponse as WorkspaceStateExportResponseType, type WorkspaceStateResponse } from "@opengeni/contracts";
2
+ export declare function canonicalWorkspaceStateJson(value: unknown): string;
3
+ export declare function createWorkspaceStateExport(state: WorkspaceStateResponse): WorkspaceStateExportResponseType;
4
+ export declare function serializeWorkspaceStateExport(state: WorkspaceStateResponse): string;
@@ -12,6 +12,10 @@ type PreferenceGovernanceIdentity = {
12
12
  activeVersion: number;
13
13
  scope: "organization" | "workspace" | "user";
14
14
  };
15
+ type CurrentPreferenceProjectionInput = {
16
+ descriptors: PreferenceGovernanceIdentity[];
17
+ truncated: boolean;
18
+ };
15
19
  type AttemptGovernanceProjectionInput = {
16
20
  status: "unavailable";
17
21
  } | {
@@ -37,6 +41,7 @@ export type WorkspaceStateProjectionInput = {
37
41
  generatedAt: string;
38
42
  workspaceAgentInstructions: string | null;
39
43
  policies: WorkspaceInstructionPolicyListResponse;
44
+ preferences: CurrentPreferenceProjectionInput;
40
45
  knowledge: KnowledgeProjectionInput | null;
41
46
  attemptGovernance?: AttemptGovernanceProjectionInput | null;
42
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "0.21.0",
3
+ "version": "0.21.7",
4
4
  "description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,17 +44,17 @@
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "@opengeni/agent-proto": "^0.3.0",
46
46
  "@opengeni/codex": "^0.2.10",
47
- "@opengeni/config": "^0.10.7",
48
- "@opengeni/contracts": "^0.36.0",
49
- "@opengeni/core": "^0.20.3",
50
- "@opengeni/db": "^0.27.0",
51
- "@opengeni/documents": "^0.3.3",
52
- "@opengeni/events": "^0.3.66",
53
- "@opengeni/github": "^0.4.23",
54
- "@opengeni/network": "^0.1.1",
55
- "@opengeni/observability": "^0.4.10",
56
- "@opengeni/runtime": "^0.18.4",
57
- "@opengeni/storage": "^0.2.60",
47
+ "@opengeni/config": "^0.10.11",
48
+ "@opengeni/contracts": "^0.38.1",
49
+ "@opengeni/core": "^0.20.10",
50
+ "@opengeni/db": "^0.27.7",
51
+ "@opengeni/documents": "^0.5.3",
52
+ "@opengeni/events": "^0.3.73",
53
+ "@opengeni/github": "^0.4.27",
54
+ "@opengeni/network": "^0.2.0",
55
+ "@opengeni/observability": "^0.4.14",
56
+ "@opengeni/runtime": "^0.18.10",
57
+ "@opengeni/storage": "^0.2.64",
58
58
  "@temporalio/client": "^1.17.0",
59
59
  "better-auth": "^1.6.14",
60
60
  "hono": "^4.12.18",
package/src/app.ts CHANGED
@@ -31,6 +31,7 @@ import { bodyLimit } from "hono/body-limit";
31
31
  import { compress } from "hono/compress";
32
32
  import { cors } from "hono/cors";
33
33
  import { HTTPException } from "hono/http-exception";
34
+ import { ApiHttpError } from "./http/api-error";
34
35
  import type { ContentfulStatusCode } from "hono/utils/http-status";
35
36
  import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
36
37
  import {
@@ -239,7 +240,11 @@ export function createAppComposition(deps: AppDependencies): {
239
240
  ],
240
241
  exposeHeaders: ["X-OpenGeni-Api-Contract", "X-OpenGeni-Correlation-Id"],
241
242
  };
242
- const publicApiCors = cors({ ...corsHeaders, credentials: false, origin: "*" });
243
+ const publicApiCors = cors({
244
+ ...corsHeaders,
245
+ credentials: false,
246
+ origin: "*",
247
+ });
243
248
  const credentialedCors = cors({
244
249
  ...corsHeaders,
245
250
  credentials: true,
@@ -565,8 +570,11 @@ export function createAppComposition(deps: AppDependencies): {
565
570
 
566
571
  app.onError((error, c) => {
567
572
  const compactionLock = codexCompactionV2ProviderLockedError(error);
573
+ const apiError = error instanceof ApiHttpError ? error : null;
568
574
  const status = compactionLock ? 422 : httpStatusForError(error);
569
- const code: ErrorCode = compactionLock ? compactionLock.code : errorCodeForStatus(status);
575
+ const code: ErrorCode = compactionLock
576
+ ? compactionLock.code
577
+ : (apiError?.code ?? errorCodeForStatus(status));
570
578
  const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
571
579
  c.header(OPENGENI_CORRELATION_HEADER, requestId);
572
580
  if (new URL(c.req.url).pathname.startsWith("/v1/")) {
@@ -578,9 +586,12 @@ export function createAppComposition(deps: AppDependencies): {
578
586
  code,
579
587
  message: compactionLock
580
588
  ? (boundedPublicMessage(compactionLock.message) ?? "Request failed.")
581
- : publicErrorMessage(error, status),
582
- retryable: retryableHttpStatus(status),
589
+ : apiError
590
+ ? (boundedPublicMessage(apiError.message) ?? "Request failed.")
591
+ : publicErrorMessage(error, status),
592
+ retryable: apiError?.retryable ?? retryableHttpStatus(status),
583
593
  requestId,
594
+ ...(apiError?.details ? { details: apiError.details } : {}),
584
595
  },
585
596
  });
586
597
  return c.json(envelope, status as ContentfulStatusCode);
@@ -0,0 +1,25 @@
1
+ import type { ErrorCode } from "@opengeni/contracts";
2
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
3
+ import { HTTPException } from "hono/http-exception";
4
+
5
+ type ApiHttpErrorOptions = {
6
+ code: ErrorCode;
7
+ message: string;
8
+ retryable?: boolean;
9
+ details?: Record<string, unknown>;
10
+ };
11
+
12
+ /** A public, structured API failure whose message and details are safe for clients. */
13
+ export class ApiHttpError extends HTTPException {
14
+ readonly code: ErrorCode;
15
+ readonly retryable: boolean | undefined;
16
+ readonly details: Record<string, unknown> | undefined;
17
+
18
+ constructor(status: number, options: ApiHttpErrorOptions) {
19
+ super(status as ContentfulStatusCode, { message: options.message });
20
+ this.name = "ApiHttpError";
21
+ this.code = options.code;
22
+ this.retryable = options.retryable;
23
+ this.details = options.details;
24
+ }
25
+ }
@@ -1,5 +1,9 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import type { Settings } from "@opengeni/config";
3
+ import {
4
+ bindConnectorDocumentDestination,
5
+ type ConnectorDocumentDestinationSelection,
6
+ } from "@opengeni/contracts/connector-destinations";
3
7
  import {
4
8
  GOOGLE_DRIVE_CREDENTIAL_LABEL,
5
9
  GOOGLE_DRIVE_CREDENTIAL_ROLE,
@@ -253,6 +257,9 @@ export async function completeGoogleDriveOAuthCallback(
253
257
  verifiedAt: new Date().toISOString(),
254
258
  accessMode: scopeDecision.accessMode,
255
259
  lifecycle: googleDriveLifecycle("active"),
260
+ ...(previousMetadata?.documentDestination
261
+ ? { documentDestination: previousMetadata.documentDestination }
262
+ : {}),
256
263
  ...(previousMetadata?.selectedSources
257
264
  ? { selectedSources: previousMetadata.selectedSources }
258
265
  : previousMetadata?.selectedSource
@@ -496,10 +503,14 @@ export async function browseGoogleDrive(
496
503
  export async function saveGoogleDriveSource(
497
504
  deps: ApiRouteDeps,
498
505
  input: {
506
+ accountId: string;
499
507
  workspaceId: string;
500
508
  subjectId: string;
501
509
  connectionId: string;
502
510
  payload: unknown;
511
+ canManageOrganizationDestination: boolean;
512
+ canManageWorkspaceDestination: boolean;
513
+ canManagePersonalDestination: boolean;
503
514
  },
504
515
  ) {
505
516
  const parsedPayload = SaveGoogleDriveSourceRequest.safeParse(input.payload);
@@ -516,7 +527,31 @@ export async function saveGoogleDriveSource(
516
527
  if (!existing) {
517
528
  throw new HTTPException(404, { message: "Google Drive connection not found" });
518
529
  }
530
+ if (existing.accountId !== input.accountId || existing.workspaceId !== input.workspaceId) {
531
+ throw new HTTPException(403, { message: "Google Drive connection authority mismatch" });
532
+ }
519
533
  await requireGoogleDriveSourceConnection(deps, existing, input.subjectId);
534
+ const destinationSelection: ConnectorDocumentDestinationSelection = payload.destination ?? {
535
+ authorityKind: "workspace",
536
+ collectionId: null,
537
+ };
538
+ if (
539
+ destinationSelection.authorityKind === "organization" &&
540
+ !input.canManageOrganizationDestination
541
+ ) {
542
+ throw new HTTPException(403, { message: "missing permission: account:admin" });
543
+ }
544
+ if (destinationSelection.authorityKind === "workspace" && !input.canManageWorkspaceDestination) {
545
+ throw new HTTPException(403, { message: "missing permission: workspace:admin" });
546
+ }
547
+ if (destinationSelection.authorityKind === "personal" && !input.canManagePersonalDestination) {
548
+ throw new HTTPException(403, { message: "personal destination requires the exact actor" });
549
+ }
550
+ const documentDestination = bindConnectorDocumentDestination(destinationSelection, {
551
+ accountId: input.accountId,
552
+ workspaceId: input.workspaceId,
553
+ initiatingSubjectId: input.subjectId,
554
+ });
520
555
  const verifiedSources = [];
521
556
  for (const source of payload.sources) {
522
557
  const sourceId = validDriveId(source.id, "source.id");
@@ -552,13 +587,14 @@ export async function saveGoogleDriveSource(
552
587
  expectedVersion: latest.version,
553
588
  metadata: GoogleDriveConnectionMetadata.parse({
554
589
  ...latestMetadata,
590
+ documentDestination,
555
591
  selectedSource: null,
556
592
  selectedSources: verifiedSources.map((verified) => ({
557
593
  id: verified.id,
558
594
  name: verified.name,
559
595
  mimeType: verified.mimeType,
560
596
  driveId: verified.driveId,
561
- targetScope: payload.targetScope,
597
+ destination: documentDestination,
562
598
  syncCadence: payload.syncCadence,
563
599
  readPolicy: payload.readPolicy,
564
600
  selectedAt: new Date().toISOString(),