@opengeni/api-router 3.0.1-canary.2 → 3.0.1-canary.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.
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-PRCUIL5L.js → chunk-RSMCZIG2.js} +3296 -2705
- package/dist/{chunk-PRCUIL5L.js.map → chunk-RSMCZIG2.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/marketplace-plugin.d.ts +7 -7
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/package.json +19 -19
- package/src/app.ts +57 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/integrations/github-app-connect.ts +25 -8
- package/src/mcp/server.ts +6 -2
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/plugins.ts +30 -2
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
createOpenGeniSlackBotInteractionClient,
|
|
8
8
|
startSlackInteractionPump,
|
|
9
9
|
startTemporalScheduleCleanupPump
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-RSMCZIG2.js";
|
|
11
11
|
|
|
12
12
|
// src/index.ts
|
|
13
13
|
import { scheduledTaskKnowledgeSource } from "@opengeni/contracts";
|
|
@@ -52,8 +52,8 @@ export declare function marketplacePlugin(url: string): {
|
|
|
52
52
|
category: string;
|
|
53
53
|
keywords: never[];
|
|
54
54
|
author: {
|
|
55
|
-
url?: never;
|
|
56
55
|
name: string;
|
|
56
|
+
url?: never;
|
|
57
57
|
email?: never;
|
|
58
58
|
};
|
|
59
59
|
version: string;
|
|
@@ -78,9 +78,9 @@ export declare function marketplacePlugin(url: string): {
|
|
|
78
78
|
installation: string;
|
|
79
79
|
upstream: {
|
|
80
80
|
source: {
|
|
81
|
-
url?: never;
|
|
82
81
|
source: string;
|
|
83
82
|
path: string;
|
|
83
|
+
url?: never;
|
|
84
84
|
};
|
|
85
85
|
policy: {
|
|
86
86
|
installation: string;
|
|
@@ -124,9 +124,9 @@ export declare function marketplacePlugin(url: string): {
|
|
|
124
124
|
installation: string;
|
|
125
125
|
upstream: {
|
|
126
126
|
source: {
|
|
127
|
-
url?: never;
|
|
128
127
|
source: string;
|
|
129
128
|
path: string;
|
|
129
|
+
url?: never;
|
|
130
130
|
};
|
|
131
131
|
policy: {
|
|
132
132
|
installation: string;
|
|
@@ -170,9 +170,9 @@ export declare function marketplacePlugin(url: string): {
|
|
|
170
170
|
installation: string;
|
|
171
171
|
upstream: {
|
|
172
172
|
source: {
|
|
173
|
-
url?: never;
|
|
174
173
|
source: string;
|
|
175
174
|
path: string;
|
|
175
|
+
url?: never;
|
|
176
176
|
};
|
|
177
177
|
policy: {
|
|
178
178
|
installation: string;
|
|
@@ -216,9 +216,9 @@ export declare function marketplacePlugin(url: string): {
|
|
|
216
216
|
installation: string;
|
|
217
217
|
upstream: {
|
|
218
218
|
source: {
|
|
219
|
-
url?: never;
|
|
220
219
|
source: string;
|
|
221
220
|
path: string;
|
|
221
|
+
url?: never;
|
|
222
222
|
};
|
|
223
223
|
policy: {
|
|
224
224
|
installation: string;
|
|
@@ -262,9 +262,9 @@ export declare function marketplacePlugin(url: string): {
|
|
|
262
262
|
installation: string;
|
|
263
263
|
upstream: {
|
|
264
264
|
source: {
|
|
265
|
-
url?: never;
|
|
266
265
|
source: string;
|
|
267
266
|
path: string;
|
|
267
|
+
url?: never;
|
|
268
268
|
};
|
|
269
269
|
policy: {
|
|
270
270
|
installation: string;
|
|
@@ -308,9 +308,9 @@ export declare function marketplacePlugin(url: string): {
|
|
|
308
308
|
installation: string;
|
|
309
309
|
upstream: {
|
|
310
310
|
source: {
|
|
311
|
-
url?: never;
|
|
312
311
|
source: string;
|
|
313
312
|
path: string;
|
|
313
|
+
url?: never;
|
|
314
314
|
};
|
|
315
315
|
policy: {
|
|
316
316
|
installation: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
2
|
+
import type { Context, Hono } from "hono";
|
|
3
|
+
export declare function admitManagedSignInMutation(context: Context, deps: ApiRouteDeps): Promise<void>;
|
|
4
|
+
export declare function registerManagedSignInMethodRoutes(app: Hono, deps: ApiRouteDeps): void;
|
|
5
|
+
/** Intercept only database-backed, product-issued explicit connect states. */
|
|
6
|
+
export declare function handleManagedSignInConnectCallback(context: Context, deps: ApiRouteDeps, provider: "google" | "github"): Promise<Response | null>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "3.0.1-canary.
|
|
3
|
+
"version": "3.0.1-canary.4",
|
|
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": {
|
|
@@ -43,24 +43,24 @@
|
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@llamaindex/liteparse": "2.14.2",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
-
"@opengeni/agent-proto": "^0.6.0-canary.
|
|
47
|
-
"@opengeni/artifact-tool": "^0.3.29-canary.
|
|
48
|
-
"@opengeni/capabilities": "^0.3.4-canary.
|
|
49
|
-
"@opengeni/codemode": "^0.5.9-canary.
|
|
50
|
-
"@opengeni/codex": "^0.2.23-canary.
|
|
51
|
-
"@opengeni/config": "^1.2.2-canary.
|
|
52
|
-
"@opengeni/contracts": "^4.1.0-canary.
|
|
53
|
-
"@opengeni/core": "^3.0.1-canary.
|
|
54
|
-
"@opengeni/db": "^5.0.1-canary.
|
|
55
|
-
"@opengeni/documents": "^0.8.30-canary.
|
|
56
|
-
"@opengeni/events": "^0.4.28-canary.
|
|
57
|
-
"@opengeni/github": "^0.7.13-canary.
|
|
58
|
-
"@opengeni/network": "^0.3.1-canary.
|
|
59
|
-
"@opengeni/observability": "^0.8.29-canary.
|
|
60
|
-
"@opengeni/runtime": "^3.0.1-canary.
|
|
61
|
-
"@opengeni/storage": "^0.2.130-canary.
|
|
62
|
-
"@opengeni/tool-gateway": "^0.1.10-canary.
|
|
63
|
-
"@opengeni/xai-subscription": "^0.1.4-canary.
|
|
46
|
+
"@opengeni/agent-proto": "^0.6.0-canary.9",
|
|
47
|
+
"@opengeni/artifact-tool": "^0.3.29-canary.4",
|
|
48
|
+
"@opengeni/capabilities": "^0.3.4-canary.4",
|
|
49
|
+
"@opengeni/codemode": "^0.5.9-canary.4",
|
|
50
|
+
"@opengeni/codex": "^0.2.23-canary.6",
|
|
51
|
+
"@opengeni/config": "^1.2.2-canary.4",
|
|
52
|
+
"@opengeni/contracts": "^4.1.0-canary.4",
|
|
53
|
+
"@opengeni/core": "^3.0.1-canary.4",
|
|
54
|
+
"@opengeni/db": "^5.0.1-canary.4",
|
|
55
|
+
"@opengeni/documents": "^0.8.30-canary.4",
|
|
56
|
+
"@opengeni/events": "^0.4.28-canary.4",
|
|
57
|
+
"@opengeni/github": "^0.7.13-canary.4",
|
|
58
|
+
"@opengeni/network": "^0.3.1-canary.21",
|
|
59
|
+
"@opengeni/observability": "^0.8.29-canary.4",
|
|
60
|
+
"@opengeni/runtime": "^3.0.1-canary.4",
|
|
61
|
+
"@opengeni/storage": "^0.2.130-canary.4",
|
|
62
|
+
"@opengeni/tool-gateway": "^0.1.10-canary.4",
|
|
63
|
+
"@opengeni/xai-subscription": "^0.1.4-canary.21",
|
|
64
64
|
"@temporalio/client": "^1.17.0",
|
|
65
65
|
"better-auth": "^1.6.14",
|
|
66
66
|
"hono": "^4.12.18",
|
package/src/app.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
runManagedAuthProvider,
|
|
107
107
|
} from "./auth/managed-auth-attempt-context";
|
|
108
108
|
import { createManagedEmailTransport } from "./auth/managed-email";
|
|
109
|
+
import { startManagedSignInNotificationDelivery } from "./auth/managed-sign-in-notifications";
|
|
109
110
|
import {
|
|
110
111
|
assertManagedEmailTransportMetadata,
|
|
111
112
|
assertOrganizationUserSetupQueryTransportConfigured,
|
|
@@ -207,6 +208,10 @@ import { registerOrganizationMembershipRoutes } from "./routes/organization-memb
|
|
|
207
208
|
import { registerOrganizationSessionRoutes } from "./routes/organization-sessions";
|
|
208
209
|
import { registerOrganizationRecoveryRoutes } from "./routes/organization-recovery";
|
|
209
210
|
import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
|
|
211
|
+
import {
|
|
212
|
+
registerManagedSignInMethodRoutes,
|
|
213
|
+
handleManagedSignInConnectCallback,
|
|
214
|
+
} from "./routes/managed-sign-in-methods";
|
|
210
215
|
import {
|
|
211
216
|
registerManagedAuthSessionSetRoutes,
|
|
212
217
|
requireManagedAuthProviderRouteAllowed,
|
|
@@ -300,6 +305,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
300
305
|
const managedAuthSessionAdapter =
|
|
301
306
|
deps.managedAuthSessionAdapter ??
|
|
302
307
|
(managedAuth ? createBetterAuthSessionAdapter(managedAuth, deps.db) : null);
|
|
308
|
+
if (managedAuth) startManagedSignInNotificationDelivery(deps.db, managedEmailTransport);
|
|
303
309
|
const objectStorage =
|
|
304
310
|
deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
|
|
305
311
|
let documentServices: DocumentServices | null = deps.documentServices ?? null;
|
|
@@ -670,10 +676,61 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
670
676
|
// wildcard handler or the provider returns its own 404 first.
|
|
671
677
|
registerManagedOnboardingRoutes(app, routeDeps);
|
|
672
678
|
registerManagedAuthSessionSetRoutes(app, routeDeps);
|
|
679
|
+
registerManagedSignInMethodRoutes(app, routeDeps);
|
|
673
680
|
if (managedAuth) {
|
|
674
681
|
app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
|
|
675
682
|
const pathname = new URL(c.req.url).pathname;
|
|
676
683
|
const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
|
|
684
|
+
if (pathname === "/v1/auth/sign-in/social" && c.req.method === "POST") {
|
|
685
|
+
const body = await c.req.raw
|
|
686
|
+
.clone()
|
|
687
|
+
.json()
|
|
688
|
+
.catch(() => null);
|
|
689
|
+
if (
|
|
690
|
+
!body ||
|
|
691
|
+
!["google", "github"].includes(body.provider) ||
|
|
692
|
+
Object.prototype.hasOwnProperty.call(body, "idToken")
|
|
693
|
+
) {
|
|
694
|
+
return c.json(
|
|
695
|
+
{
|
|
696
|
+
code: "SIGN_IN_METHOD_OAUTH_REDIRECT_REQUIRED",
|
|
697
|
+
message: "Use the browser OAuth sign-in redirect",
|
|
698
|
+
},
|
|
699
|
+
403,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (oauthCallbackProvider) {
|
|
704
|
+
const connectResponse = await handleManagedSignInConnectCallback(
|
|
705
|
+
c,
|
|
706
|
+
routeDeps,
|
|
707
|
+
oauthCallbackProvider,
|
|
708
|
+
);
|
|
709
|
+
if (connectResponse) return connectResponse;
|
|
710
|
+
}
|
|
711
|
+
if (
|
|
712
|
+
new Set([
|
|
713
|
+
"link-social",
|
|
714
|
+
"unlink-account",
|
|
715
|
+
"list-accounts",
|
|
716
|
+
"set-password",
|
|
717
|
+
"change-password",
|
|
718
|
+
"change-email",
|
|
719
|
+
"update-user",
|
|
720
|
+
"delete-user",
|
|
721
|
+
"get-access-token",
|
|
722
|
+
"refresh-token",
|
|
723
|
+
"account-info",
|
|
724
|
+
]).has(pathname.slice("/v1/auth/".length))
|
|
725
|
+
) {
|
|
726
|
+
return c.json(
|
|
727
|
+
{
|
|
728
|
+
code: "SIGN_IN_METHOD_PRODUCT_ROUTE_REQUIRED",
|
|
729
|
+
message: "Use personal sign-in method settings",
|
|
730
|
+
},
|
|
731
|
+
403,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
677
734
|
if (deps.settings.managedAuthSessionSetMode === "legacy") {
|
|
678
735
|
return oauthCallbackProvider
|
|
679
736
|
? await runManagedAuthProvider(
|
|
@@ -11,6 +11,15 @@ type ManagedAuthAttemptContext =
|
|
|
11
11
|
| { kind: "discard_provider_session" };
|
|
12
12
|
|
|
13
13
|
const managedAuthAttemptStorage = new AsyncLocalStorage<ManagedAuthAttemptContext>();
|
|
14
|
+
const managedLinkIntentStorage = new AsyncLocalStorage<string>();
|
|
15
|
+
|
|
16
|
+
export function runManagedSignInConnect<T>(intentId: string, action: () => T): T {
|
|
17
|
+
return managedLinkIntentStorage.run(intentId, action);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function currentManagedSignInConnectIntent(): string | undefined {
|
|
21
|
+
return managedLinkIntentStorage.getStore();
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
export function runManagedAuthAttempt<T>(
|
|
16
25
|
transactionId: string,
|
|
@@ -8,6 +8,17 @@ import type { ManagedAuth } from "@opengeni/core";
|
|
|
8
8
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
9
9
|
import { runManagedAuthAttempt } from "./managed-auth-attempt-context";
|
|
10
10
|
|
|
11
|
+
/** Server-only proof for a product-owned endpoint; never returned to a browser. */
|
|
12
|
+
export async function managedAuthSelectedProofHeaders(
|
|
13
|
+
auth: ManagedAuth,
|
|
14
|
+
token: string,
|
|
15
|
+
): Promise<Headers> {
|
|
16
|
+
const context = await auth.$context;
|
|
17
|
+
return new Headers({
|
|
18
|
+
cookie: `${context.authCookies.sessionToken.name}=${signedCookieValue(token, context.secret)}`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export function createBetterAuthSessionAdapter(
|
|
12
23
|
auth: ManagedAuth,
|
|
13
24
|
db: Database,
|
package/src/auth/managed-auth.ts
CHANGED
|
@@ -14,16 +14,18 @@ import {
|
|
|
14
14
|
} from "@opengeni/db/canonical-human-identities";
|
|
15
15
|
import { betterAuth } from "better-auth";
|
|
16
16
|
import { createEmailVerificationToken } from "better-auth/api";
|
|
17
|
-
import { hashPassword } from "better-auth/crypto";
|
|
17
|
+
import { hashPassword, verifyPassword } from "better-auth/crypto";
|
|
18
18
|
import { sql } from "drizzle-orm";
|
|
19
19
|
import { Pool } from "pg";
|
|
20
20
|
|
|
21
21
|
import { decideCanonicalHumanSessionAdmission } from "./canonical-human-session-admission";
|
|
22
|
+
import { deliverManagedSignInNotification } from "./managed-sign-in-notifications";
|
|
22
23
|
import {
|
|
23
24
|
currentManagedAuthProviderId,
|
|
24
25
|
currentManagedAuthAttemptId,
|
|
25
26
|
recordCurrentManagedAuthSession,
|
|
26
27
|
shouldDiscardCurrentManagedAuthProviderSession,
|
|
28
|
+
currentManagedSignInConnectIntent,
|
|
27
29
|
} from "./managed-auth-attempt-context";
|
|
28
30
|
|
|
29
31
|
// `ManagedAuth` (the Better Auth `Auth<any>` alias) is owned by @opengeni/core
|
|
@@ -66,6 +68,10 @@ export async function hashManagedAuthPassword(password: string): Promise<string>
|
|
|
66
68
|
return await hashPassword(password);
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
export async function verifyManagedAuthPassword(password: string, hash: string): Promise<boolean> {
|
|
72
|
+
return await verifyPassword({ password, hash });
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
export function createManagedAuth(
|
|
70
76
|
settings: Settings,
|
|
71
77
|
db: Database,
|
|
@@ -180,7 +186,18 @@ export function createManagedAuth(
|
|
|
180
186
|
updatedAt: "updated_at",
|
|
181
187
|
},
|
|
182
188
|
accountLinking: {
|
|
183
|
-
enabled:
|
|
189
|
+
enabled: true,
|
|
190
|
+
requireLocalEmailVerified: true,
|
|
191
|
+
trustedProviders: [],
|
|
192
|
+
allowDifferentEmails: false,
|
|
193
|
+
},
|
|
194
|
+
additionalFields: {
|
|
195
|
+
managedLinkIntentId: {
|
|
196
|
+
type: "string",
|
|
197
|
+
fieldName: "managed_link_intent_id",
|
|
198
|
+
input: false,
|
|
199
|
+
returned: false,
|
|
200
|
+
},
|
|
184
201
|
},
|
|
185
202
|
encryptOAuthTokens: true,
|
|
186
203
|
storeStateStrategy: "database",
|
|
@@ -264,6 +281,26 @@ export function createManagedAuth(
|
|
|
264
281
|
},
|
|
265
282
|
},
|
|
266
283
|
databaseHooks: {
|
|
284
|
+
account: {
|
|
285
|
+
create: {
|
|
286
|
+
before: async (account) => ({
|
|
287
|
+
data: {
|
|
288
|
+
...account,
|
|
289
|
+
...(currentManagedSignInConnectIntent()
|
|
290
|
+
? { managedLinkIntentId: currentManagedSignInConnectIntent() }
|
|
291
|
+
: {}),
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
after: async (account) => {
|
|
295
|
+
if (
|
|
296
|
+
!["google", "github"].includes(account.providerId) ||
|
|
297
|
+
currentManagedSignInConnectIntent()
|
|
298
|
+
)
|
|
299
|
+
return;
|
|
300
|
+
await deliverManagedSignInNotification(db, managedEmailTransport, account.id);
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
267
304
|
session: {
|
|
268
305
|
create: {
|
|
269
306
|
before: async (session) => {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ManagedEmailTransport } from "@opengeni/core";
|
|
2
|
+
import type { Database } from "@opengeni/db";
|
|
3
|
+
import { sql } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const Claim = z.object({
|
|
7
|
+
operationId: z.string().uuid(),
|
|
8
|
+
leaseId: z.string().uuid(),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
provider: z.enum(["password", "google", "github"]),
|
|
11
|
+
sender: z.string(),
|
|
12
|
+
});
|
|
13
|
+
const started = new WeakSet<object>();
|
|
14
|
+
|
|
15
|
+
export async function deliverManagedSignInNotification(
|
|
16
|
+
db: Database,
|
|
17
|
+
transport: ManagedEmailTransport,
|
|
18
|
+
operationId: string | null = null,
|
|
19
|
+
): Promise<"sent" | "failed" | "outcome_unknown"> {
|
|
20
|
+
try {
|
|
21
|
+
const result = await db.execute(
|
|
22
|
+
sql`select claim_managed_sign_in_notification(${operationId}::uuid,${transport.sender},${transport.idempotency.scope},${transport.idempotency.retentionSeconds}::integer) as claim`,
|
|
23
|
+
);
|
|
24
|
+
const row = (Array.isArray(result) ? result : result.rows)[0] as { claim: unknown } | undefined;
|
|
25
|
+
if (!row?.claim) return "outcome_unknown";
|
|
26
|
+
const claim = Claim.parse(row.claim);
|
|
27
|
+
let status: "sent" | "failed" | "outcome_unknown" = "outcome_unknown";
|
|
28
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
29
|
+
try {
|
|
30
|
+
const response = await Promise.race([
|
|
31
|
+
transport.send({
|
|
32
|
+
kind: "sign_in_method_changed",
|
|
33
|
+
from: claim.sender,
|
|
34
|
+
to: claim.email,
|
|
35
|
+
idempotencyKey: `sign-in-method:${claim.operationId}`,
|
|
36
|
+
subject: "Your OpenGeni sign-in methods changed",
|
|
37
|
+
text: `Your OpenGeni ${claim.provider} sign-in method changed. If this was not you, reset your password and contact your administrator.`,
|
|
38
|
+
html: `<p>Your OpenGeni ${claim.provider} sign-in method changed. If this was not you, reset your password and contact your administrator.</p>`,
|
|
39
|
+
}),
|
|
40
|
+
new Promise<{ status: "outcome_unknown" }>((resolve) => {
|
|
41
|
+
timer = setTimeout(() => resolve({ status: "outcome_unknown" }), 5_000);
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
status = response.status;
|
|
45
|
+
} catch {
|
|
46
|
+
/* Retain the durable unknown claim for idempotent retry. */
|
|
47
|
+
} finally {
|
|
48
|
+
if (timer) clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
await db.execute(
|
|
51
|
+
sql`select settle_managed_sign_in_notification(${claim.operationId}::uuid,${claim.leaseId}::uuid,${status})`,
|
|
52
|
+
);
|
|
53
|
+
return status;
|
|
54
|
+
} catch {
|
|
55
|
+
// Security state is already committed. A failed claim/ack must never be
|
|
56
|
+
// reported as a failed method change; its durable obligation remains live.
|
|
57
|
+
return "outcome_unknown";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function startManagedSignInNotificationDelivery(
|
|
62
|
+
db: Database,
|
|
63
|
+
transport: ManagedEmailTransport,
|
|
64
|
+
): void {
|
|
65
|
+
if (started.has(db as object)) return;
|
|
66
|
+
started.add(db as object);
|
|
67
|
+
let active = false;
|
|
68
|
+
const drain = async () => {
|
|
69
|
+
if (active) return;
|
|
70
|
+
active = true;
|
|
71
|
+
try {
|
|
72
|
+
for (let count = 0; count < 10; count++)
|
|
73
|
+
if ((await deliverManagedSignInNotification(db, transport)) === "outcome_unknown") break;
|
|
74
|
+
} finally {
|
|
75
|
+
active = false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const timer = setInterval(() => void drain(), 60_000);
|
|
79
|
+
(timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
|
|
80
|
+
void drain();
|
|
81
|
+
}
|
|
@@ -210,6 +210,18 @@ export async function completeGitHubAppConnect(
|
|
|
210
210
|
state.providerId,
|
|
211
211
|
acquiring,
|
|
212
212
|
);
|
|
213
|
+
// Recover a repeated install callback only while its next stage is still
|
|
214
|
+
// current. A completed/superseded attempt returns home without reopening OAuth.
|
|
215
|
+
if (
|
|
216
|
+
claim.status === "replayed" &&
|
|
217
|
+
state.phase === "install" &&
|
|
218
|
+
claim.attempt.revision === stored.attempt.revision &&
|
|
219
|
+
claim.attempt.nextAction.type === "authorize"
|
|
220
|
+
)
|
|
221
|
+
return new Response(null, {
|
|
222
|
+
status: 302,
|
|
223
|
+
headers: { Location: claim.attempt.nextAction.url },
|
|
224
|
+
});
|
|
213
225
|
if (claim.status !== "replayed") {
|
|
214
226
|
const provider =
|
|
215
227
|
state.providerId === "github-lens" ? deps.prReviewGithubAppApi : deps.githubAppApi;
|
|
@@ -390,14 +402,19 @@ export async function completeGitHubAppConnect(
|
|
|
390
402
|
},
|
|
391
403
|
};
|
|
392
404
|
}
|
|
393
|
-
|
|
394
|
-
await finishOAuthAcquisition(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
405
|
+
const completed = acquiring
|
|
406
|
+
? await finishOAuthAcquisition(
|
|
407
|
+
deps.db,
|
|
408
|
+
state,
|
|
409
|
+
{ ...operation, commit: prepared.commit },
|
|
410
|
+
state.providerId,
|
|
411
|
+
)
|
|
412
|
+
: await finishConnectOperation(deps.db, state, { ...operation, commit: prepared.commit });
|
|
413
|
+
// Installation is only the first half of consent. Keep this browser on
|
|
414
|
+
// the exact authorization stage committed above; polling still waits for
|
|
415
|
+
// the final owner proof before reporting connection completion.
|
|
416
|
+
if (state.phase === "install" && completed.nextAction.type === "authorize")
|
|
417
|
+
return new Response(null, { status: 302, headers: { Location: completed.nextAction.url } });
|
|
401
418
|
}
|
|
402
419
|
} catch {
|
|
403
420
|
// Preserve unknown outcomes; never replay provider authorization to recover.
|
package/src/mcp/server.ts
CHANGED
|
@@ -5787,7 +5787,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
5787
5787
|
"capability_catalog_search",
|
|
5788
5788
|
{
|
|
5789
5789
|
description:
|
|
5790
|
-
"
|
|
5790
|
+
"Find integrations in OpenGeni's reviewed workspace catalog when the user asks to add one or needed access is missing. Search by integration name or task outcome. Results describe setup status and provide setup.nextAction when human setup can be requested. Use available tools directly for ready candidates. This reads metadata only and does not connect or authorize anything.",
|
|
5791
5791
|
inputSchema: {
|
|
5792
5792
|
query: z4.string().min(1).max(500),
|
|
5793
5793
|
limit: z4.number().int().min(1).max(20).optional(),
|
|
@@ -5817,6 +5817,10 @@ function registerCapabilityDiscoveryTools(
|
|
|
5817
5817
|
setup: {
|
|
5818
5818
|
...setups[index]!,
|
|
5819
5819
|
requiredVariables: capabilityRequiredVariables(item),
|
|
5820
|
+
nextAction:
|
|
5821
|
+
setups[index]!.status === "authorization_required"
|
|
5822
|
+
? { toolName: "capability_authorization_request", capabilityId: item.id }
|
|
5823
|
+
: null,
|
|
5820
5824
|
},
|
|
5821
5825
|
}));
|
|
5822
5826
|
return json({ query, matches });
|
|
@@ -5827,7 +5831,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
5827
5831
|
"capability_authorization_request",
|
|
5828
5832
|
{
|
|
5829
5833
|
description:
|
|
5830
|
-
"
|
|
5834
|
+
"Show a Connect card in this chat for a suitable capability returned by capability_catalog_search with setup.nextAction. Supply its capability ID and a brief rationale explaining how it helps the task; no separate confirmation is needed before showing the card. Requesting setup needs no integration-management permission and grants no access. The authenticated human completes setup through the card; never ask them to paste credentials into chat. Do not request another card for the same pending setup, or for a candidate reported ready or unavailable.",
|
|
5831
5835
|
inputSchema: {
|
|
5832
5836
|
capabilityId: z4.string().min(1).max(512),
|
|
5833
5837
|
rationale: z4.string().min(1).max(2000),
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
import type { Context, Hono } from "hono";
|
|
24
24
|
import { HTTPException } from "hono/http-exception";
|
|
25
25
|
import { z } from "zod";
|
|
26
|
+
import { admitManagedSignInMutation } from "./managed-sign-in-methods";
|
|
27
|
+
import { managedAuthSecretRequestDigest } from "@opengeni/core/managed-auth-session-sets";
|
|
28
|
+
import { deliverManagedSignInNotification } from "../auth/managed-sign-in-notifications";
|
|
26
29
|
|
|
27
30
|
const BindingId = z.string().uuid();
|
|
28
31
|
|
|
@@ -86,6 +89,7 @@ async function mutate(
|
|
|
86
89
|
operationId: string;
|
|
87
90
|
expectedIdentityRevision: number;
|
|
88
91
|
reason: string;
|
|
92
|
+
verifiedManagedRecovery?: boolean;
|
|
89
93
|
},
|
|
90
94
|
): Promise<Response> {
|
|
91
95
|
const identity = await requestIdentity(context, deps);
|
|
@@ -104,10 +108,27 @@ async function mutate(
|
|
|
104
108
|
...(input.providerId ? { providerId: input.providerId } : {}),
|
|
105
109
|
...(input.providerAccountId ? { providerAccountId: input.providerAccountId } : {}),
|
|
106
110
|
reason: input.reason,
|
|
111
|
+
...(input.verifiedManagedRecovery
|
|
112
|
+
? {
|
|
113
|
+
verifiedRecovery: {
|
|
114
|
+
authSessionId: identity.authSessionId,
|
|
115
|
+
requestDigest: managedAuthSecretRequestDigest(
|
|
116
|
+
deps.settings.betterAuthSecret!,
|
|
117
|
+
input,
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
107
122
|
...(actorFence ? { actorFence } : {}),
|
|
108
123
|
}),
|
|
109
124
|
);
|
|
110
125
|
if (actorFence) markManagedAuthRequestActorTransitionApplied(context.req.raw);
|
|
126
|
+
if (input.verifiedManagedRecovery)
|
|
127
|
+
await deliverManagedSignInNotification(
|
|
128
|
+
deps.db,
|
|
129
|
+
deps.managedEmailTransport,
|
|
130
|
+
input.operationId,
|
|
131
|
+
);
|
|
111
132
|
return context.json(response);
|
|
112
133
|
} catch (error) {
|
|
113
134
|
return identityError(context, error);
|
|
@@ -132,6 +153,11 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
132
153
|
|
|
133
154
|
app.post(`${base}/login-bindings`, async (context) => {
|
|
134
155
|
const request = await parseBody(context, LinkCanonicalHumanLoginBindingRequest);
|
|
156
|
+
if (["google", "github", "credential"].includes(request.providerId.trim().toLowerCase())) {
|
|
157
|
+
throw new HTTPException(403, {
|
|
158
|
+
message: "Use personal sign-in method settings with fresh provider proof",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
135
161
|
return await mutate(context, deps, {
|
|
136
162
|
operationType: "link",
|
|
137
163
|
...request,
|
|
@@ -141,6 +167,19 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
141
167
|
|
|
142
168
|
app.delete(`${base}/login-bindings/:bindingId`, async (context) => {
|
|
143
169
|
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
170
|
+
const actor = await requestIdentity(context, deps);
|
|
171
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
172
|
+
if (
|
|
173
|
+
projection.loginBindings.some(
|
|
174
|
+
(binding) =>
|
|
175
|
+
binding.id === bindingId(context) &&
|
|
176
|
+
["google", "github", "credential"].includes(binding.providerId),
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
throw new HTTPException(403, {
|
|
180
|
+
message: "Use personal sign-in method settings with last-method protection",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
144
183
|
return await mutate(context, deps, {
|
|
145
184
|
operationType: "unlink",
|
|
146
185
|
bindingId: bindingId(context),
|
|
@@ -151,6 +190,19 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
151
190
|
|
|
152
191
|
app.post(`${base}/login-bindings/:bindingId/recovery`, async (context) => {
|
|
153
192
|
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
193
|
+
const actor = await requestIdentity(context, deps);
|
|
194
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
195
|
+
if (
|
|
196
|
+
projection.loginBindings.some(
|
|
197
|
+
(binding) =>
|
|
198
|
+
binding.id === bindingId(context) &&
|
|
199
|
+
["google", "github", "credential"].includes(binding.providerId),
|
|
200
|
+
)
|
|
201
|
+
) {
|
|
202
|
+
throw new HTTPException(403, {
|
|
203
|
+
message: "Managed-provider recovery cannot be initiated through raw binding management",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
154
206
|
return await mutate(context, deps, {
|
|
155
207
|
operationType: "begin_recovery",
|
|
156
208
|
bindingId: bindingId(context),
|
|
@@ -161,11 +213,20 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
161
213
|
|
|
162
214
|
app.post(`${base}/login-bindings/:bindingId/recovery/complete`, async (context) => {
|
|
163
215
|
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
216
|
+
const actor = await requestIdentity(context, deps);
|
|
217
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
218
|
+
const managed = projection.loginBindings.some(
|
|
219
|
+
(binding) =>
|
|
220
|
+
binding.id === bindingId(context) &&
|
|
221
|
+
["google", "github", "credential"].includes(binding.providerId),
|
|
222
|
+
);
|
|
223
|
+
if (managed) await admitManagedSignInMutation(context, deps);
|
|
164
224
|
return await mutate(context, deps, {
|
|
165
225
|
operationType: "recover",
|
|
166
226
|
bindingId: bindingId(context),
|
|
167
227
|
...request,
|
|
168
228
|
operationId: request.operationId ?? randomUUID(),
|
|
229
|
+
...(managed ? { verifiedManagedRecovery: true } : {}),
|
|
169
230
|
});
|
|
170
231
|
});
|
|
171
232
|
}
|