@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.
- package/dist/app.js +1 -1
- package/dist/{chunk-5EEC7Q6C.js → chunk-RIFVFI3J.js} +1217 -764
- package/dist/chunk-RIFVFI3J.js.map +1 -0
- package/dist/http/api-error.d.ts +16 -0
- package/dist/index.js +1 -1
- package/dist/integrations/google-drive.d.ts +4 -0
- package/dist/integrations/oauth-client.d.ts +5 -0
- package/dist/workspace-state-export.d.ts +4 -0
- package/dist/workspace-state-projection.d.ts +5 -0
- package/package.json +12 -12
- package/src/app.ts +15 -4
- package/src/http/api-error.ts +25 -0
- package/src/integrations/google-drive.ts +37 -1
- package/src/integrations/oauth-client.ts +539 -98
- package/src/model-catalog.ts +1 -0
- package/src/routes/connections.ts +17 -3
- package/src/routes/workspace-state.ts +101 -76
- package/src/workspace-state-export.ts +49 -0
- package/src/workspace-state-projection.ts +23 -0
- package/dist/chunk-5EEC7Q6C.js.map +0 -1
|
@@ -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
|
@@ -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.
|
|
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.
|
|
48
|
-
"@opengeni/contracts": "^0.
|
|
49
|
-
"@opengeni/core": "^0.20.
|
|
50
|
-
"@opengeni/db": "^0.27.
|
|
51
|
-
"@opengeni/documents": "^0.
|
|
52
|
-
"@opengeni/events": "^0.3.
|
|
53
|
-
"@opengeni/github": "^0.4.
|
|
54
|
-
"@opengeni/network": "^0.
|
|
55
|
-
"@opengeni/observability": "^0.4.
|
|
56
|
-
"@opengeni/runtime": "^0.18.
|
|
57
|
-
"@opengeni/storage": "^0.2.
|
|
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({
|
|
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
|
|
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
|
-
:
|
|
582
|
-
|
|
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
|
-
|
|
597
|
+
destination: documentDestination,
|
|
562
598
|
syncCadence: payload.syncCadence,
|
|
563
599
|
readPolicy: payload.readPolicy,
|
|
564
600
|
selectedAt: new Date().toISOString(),
|