@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
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ManagedSignInMethods,
|
|
3
|
+
ManagedSignInPasswordMutation,
|
|
4
|
+
ManagedSignInProviderMutation,
|
|
5
|
+
ManagedSignInMutationResponse,
|
|
6
|
+
} from "@opengeni/contracts/managed-sign-in-methods";
|
|
7
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
8
|
+
import {
|
|
9
|
+
requireCanonicalHumanRequestIdentity,
|
|
10
|
+
getManagedAuthRequestActorLeaseStamp,
|
|
11
|
+
markManagedAuthRequestActorTransitionApplied,
|
|
12
|
+
} from "@opengeni/core/canonical-human-identities";
|
|
13
|
+
import {
|
|
14
|
+
MANAGED_AUTH_SESSION_SET_COOKIE,
|
|
15
|
+
managedAuthSha256,
|
|
16
|
+
managedAuthSecretRequestDigest,
|
|
17
|
+
requireManagedAuthMutationAdmission,
|
|
18
|
+
} from "@opengeni/core/managed-auth-session-sets";
|
|
19
|
+
import { getManagedAuthSessionSetSnapshot } from "@opengeni/db/managed-auth-session-sets";
|
|
20
|
+
import { getCanonicalHumanIdentityProjection } from "@opengeni/db/canonical-human-identities";
|
|
21
|
+
import { sql } from "drizzle-orm";
|
|
22
|
+
import type { Context, Hono } from "hono";
|
|
23
|
+
import { getCookie } from "hono/cookie";
|
|
24
|
+
import { HTTPException } from "hono/http-exception";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import {
|
|
27
|
+
isolatedManagedAuthOAuthCallbackRequest,
|
|
28
|
+
hashManagedAuthPassword,
|
|
29
|
+
verifyManagedAuthPassword,
|
|
30
|
+
} from "../auth/managed-auth";
|
|
31
|
+
import { runManagedSignInConnect } from "../auth/managed-auth-attempt-context";
|
|
32
|
+
import { deliverManagedSignInNotification } from "../auth/managed-sign-in-notifications";
|
|
33
|
+
import { managedAuthSelectedProofHeaders } from "../auth/managed-auth-session-adapter";
|
|
34
|
+
import { scrubManagedAuthProviderResponse } from "./managed-auth-session-sets";
|
|
35
|
+
|
|
36
|
+
const ConnectState = z.object({
|
|
37
|
+
opengeniSignInMethod: z.object({
|
|
38
|
+
intentId: z.string().uuid(),
|
|
39
|
+
provider: z.enum(["google", "github"]),
|
|
40
|
+
}),
|
|
41
|
+
link: z.object({ userId: z.string().min(1), email: z.string().email() }),
|
|
42
|
+
callbackURL: z.string().url(),
|
|
43
|
+
errorURL: z.string().url(),
|
|
44
|
+
});
|
|
45
|
+
const ConnectIntent = z.object({
|
|
46
|
+
authUserId: z.string().min(1),
|
|
47
|
+
authSessionId: z.string().min(1),
|
|
48
|
+
expectedIdentityId: z.string().uuid(),
|
|
49
|
+
actorFence: z
|
|
50
|
+
.object({
|
|
51
|
+
authorityHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
52
|
+
actorEpoch: z.string().regex(/^[1-9][0-9]*$/),
|
|
53
|
+
requestId: z.string().uuid(),
|
|
54
|
+
})
|
|
55
|
+
.optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function rows<T>(deps: ApiRouteDeps, query: ReturnType<typeof sql>): Promise<T[]> {
|
|
59
|
+
const result = await deps.db.execute(query);
|
|
60
|
+
return (Array.isArray(result) ? result : (result as { rows: T[] }).rows) as T[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function identity(context: Context, deps: ApiRouteDeps) {
|
|
64
|
+
return requireCanonicalHumanRequestIdentity(context, {
|
|
65
|
+
db: deps.db,
|
|
66
|
+
managedAuth: deps.managedAuth,
|
|
67
|
+
managedAuthSessionAdapter: deps.managedAuthSessionAdapter,
|
|
68
|
+
managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function admitManagedSignInMutation(context: Context, deps: ApiRouteDeps) {
|
|
73
|
+
const origin = deps.settings.publicBaseUrl ? new URL(deps.settings.publicBaseUrl).origin : null;
|
|
74
|
+
if (
|
|
75
|
+
!origin ||
|
|
76
|
+
context.req.header("origin") !== origin ||
|
|
77
|
+
context.req.header("sec-fetch-site") !== "same-origin" ||
|
|
78
|
+
context.req.header("content-type")?.split(";", 1)[0] !== "application/json"
|
|
79
|
+
) {
|
|
80
|
+
throw new HTTPException(403, { message: "Same-origin browser mutation required" });
|
|
81
|
+
}
|
|
82
|
+
const authority = getCookie(context, MANAGED_AUTH_SESSION_SET_COOKIE);
|
|
83
|
+
if (deps.settings.managedAuthSessionSetMode !== "legacy" && authority) {
|
|
84
|
+
const snapshot = await getManagedAuthSessionSetSnapshot(deps.db, {
|
|
85
|
+
authorityHash: managedAuthSha256(authority),
|
|
86
|
+
mode: deps.settings.managedAuthSessionSetMode,
|
|
87
|
+
readOnly: true,
|
|
88
|
+
});
|
|
89
|
+
if (!snapshot) throw new HTTPException(401);
|
|
90
|
+
try {
|
|
91
|
+
requireManagedAuthMutationAdmission({
|
|
92
|
+
request: context.req.raw,
|
|
93
|
+
allowedOrigins: [origin],
|
|
94
|
+
authority,
|
|
95
|
+
signingSecret: deps.settings.betterAuthSecret!,
|
|
96
|
+
expectedGeneration: snapshot.projection.generation,
|
|
97
|
+
});
|
|
98
|
+
} catch {
|
|
99
|
+
throw new HTTPException(403, { message: "Browser session-set CSRF validation failed" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function configured(deps: ApiRouteDeps, provider: "credential" | "google" | "github") {
|
|
105
|
+
return (
|
|
106
|
+
provider === "credential" ||
|
|
107
|
+
(provider === "google"
|
|
108
|
+
? Boolean(
|
|
109
|
+
deps.settings.managedAuthGoogleClientId && deps.settings.managedAuthGoogleClientSecret,
|
|
110
|
+
)
|
|
111
|
+
: Boolean(
|
|
112
|
+
deps.settings.managedAuthGithubClientId && deps.settings.managedAuthGithubClientSecret,
|
|
113
|
+
))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function mutationError(context: Context, error: unknown): Response {
|
|
118
|
+
let candidate: unknown = error;
|
|
119
|
+
for (
|
|
120
|
+
let depth = 0;
|
|
121
|
+
depth < 5 && candidate instanceof Error;
|
|
122
|
+
depth++, candidate = candidate.cause
|
|
123
|
+
) {
|
|
124
|
+
const code = candidate.message.match(/SIGN_IN_METHOD_[A-Z_]+/)?.[0];
|
|
125
|
+
if (code)
|
|
126
|
+
return context.json(
|
|
127
|
+
{ code, message: code.toLowerCase().replaceAll("_", " ") },
|
|
128
|
+
code.includes("CONFLICT") ||
|
|
129
|
+
code.includes("REUSED") ||
|
|
130
|
+
code.includes("CHANGED") ||
|
|
131
|
+
code.includes("COLLISION") ||
|
|
132
|
+
code.includes("ALREADY")
|
|
133
|
+
? 409
|
|
134
|
+
: 403,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function registerManagedSignInMethodRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
141
|
+
const base = "/v1/auth/sign-in-methods";
|
|
142
|
+
app.use(`${base}*`, async (context, next) => {
|
|
143
|
+
context.header("cache-control", "no-store");
|
|
144
|
+
await next();
|
|
145
|
+
});
|
|
146
|
+
app.get(base, async (context) => {
|
|
147
|
+
const actor = await identity(context, deps);
|
|
148
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
149
|
+
const [user] = await rows<{ email: string; emailVerified: boolean; fresh: boolean }>(
|
|
150
|
+
deps,
|
|
151
|
+
sql`
|
|
152
|
+
select u.email, u.email_verified as "emailVerified", s.created_at > clock_timestamp()-interval '5 minutes' as fresh
|
|
153
|
+
from auth_users u join auth_sessions s on s.user_id=u.id where u.id=${actor.authUserId} and s.id=${actor.authSessionId}`,
|
|
154
|
+
);
|
|
155
|
+
if (!user) throw new HTTPException(401);
|
|
156
|
+
const accounts = await rows<{ provider: string; usable: boolean }>(
|
|
157
|
+
deps,
|
|
158
|
+
sql`
|
|
159
|
+
select provider_id as provider, (provider_id <> 'credential' or length(password)>0) as usable from auth_identities where user_id=${actor.authUserId}`,
|
|
160
|
+
);
|
|
161
|
+
const connected = (provider: string) =>
|
|
162
|
+
accounts.some((a) => a.provider === provider && a.usable) &&
|
|
163
|
+
projection.loginBindings.some((b) => b.providerId === provider && b.status === "active");
|
|
164
|
+
const count = (["credential", "google", "github"] as const).filter(
|
|
165
|
+
(p) => connected(p) && configured(deps, p),
|
|
166
|
+
).length;
|
|
167
|
+
return context.json(
|
|
168
|
+
ManagedSignInMethods.parse({
|
|
169
|
+
...user,
|
|
170
|
+
identityId: projection.activeIdentity.id,
|
|
171
|
+
identityRevision: projection.activeIdentity.identityRevision,
|
|
172
|
+
freshAuthenticationRequired: !user.fresh,
|
|
173
|
+
methods: (["credential", "google", "github"] as const).map((provider) => ({
|
|
174
|
+
provider,
|
|
175
|
+
connected: connected(provider),
|
|
176
|
+
available: configured(deps, provider),
|
|
177
|
+
canDisconnect:
|
|
178
|
+
provider !== "credential" &&
|
|
179
|
+
connected(provider) &&
|
|
180
|
+
count - (configured(deps, provider) ? 1 : 0) > 0,
|
|
181
|
+
implicitRelinkingSuppressed:
|
|
182
|
+
!connected(provider) &&
|
|
183
|
+
projection.loginBindings.some(
|
|
184
|
+
(b) => b.providerId === provider && b.status !== "active",
|
|
185
|
+
),
|
|
186
|
+
})),
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
for (const kind of ["connect", "disconnect", "password"] as const) {
|
|
191
|
+
app.post(`${base}/${kind}`, async (context) => {
|
|
192
|
+
await admitManagedSignInMutation(context, deps);
|
|
193
|
+
const actor = await identity(context, deps);
|
|
194
|
+
const actorFence = getManagedAuthRequestActorLeaseStamp(context.req.raw);
|
|
195
|
+
if (deps.settings.managedAuthSessionSetMode !== "legacy" && !actorFence)
|
|
196
|
+
throw new HTTPException(409, { message: "Managed actor fence required" });
|
|
197
|
+
const schema =
|
|
198
|
+
kind === "password" ? ManagedSignInPasswordMutation : ManagedSignInProviderMutation;
|
|
199
|
+
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
200
|
+
if (!parsed.success)
|
|
201
|
+
throw new HTTPException(422, { message: "Invalid sign-in method request" });
|
|
202
|
+
const body = parsed.data;
|
|
203
|
+
const currentIdentity = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
204
|
+
if (currentIdentity.activeIdentity.id !== body.expectedIdentityId)
|
|
205
|
+
return context.json(
|
|
206
|
+
{
|
|
207
|
+
code: "SIGN_IN_METHOD_IDENTITY_CHANGED",
|
|
208
|
+
message: "The signed-in account changed; reload security settings",
|
|
209
|
+
},
|
|
210
|
+
409,
|
|
211
|
+
);
|
|
212
|
+
const [user] = await rows<{ email: string; token: string }>(
|
|
213
|
+
deps,
|
|
214
|
+
sql`select u.email,s.token from auth_users u join auth_sessions s on s.user_id=u.id where u.id=${actor.authUserId} and s.id=${actor.authSessionId}`,
|
|
215
|
+
);
|
|
216
|
+
if (!user) throw new HTTPException(401);
|
|
217
|
+
const request: Record<string, unknown> = {
|
|
218
|
+
kind,
|
|
219
|
+
operationId: body.operationId,
|
|
220
|
+
requestDigest: managedAuthSecretRequestDigest(deps.settings.betterAuthSecret!, {
|
|
221
|
+
kind,
|
|
222
|
+
...body,
|
|
223
|
+
}),
|
|
224
|
+
expectedIdentityRevision: body.expectedIdentityRevision,
|
|
225
|
+
expectedIdentityId: body.expectedIdentityId,
|
|
226
|
+
usableProviders: (["credential", "google", "github"] as const).filter((p) =>
|
|
227
|
+
configured(deps, p),
|
|
228
|
+
),
|
|
229
|
+
...(actorFence ? { actorFence } : {}),
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
const [prior] = await rows<{ result: unknown }>(
|
|
233
|
+
deps,
|
|
234
|
+
sql`select replay_managed_sign_in_method(${actor.authUserId},${actor.authSessionId},${JSON.stringify(request)}::jsonb) result`,
|
|
235
|
+
);
|
|
236
|
+
if (prior?.result)
|
|
237
|
+
return context.json(
|
|
238
|
+
kind === "connect"
|
|
239
|
+
? {
|
|
240
|
+
url: new URL(
|
|
241
|
+
"/settings/security?signInMethod=connected",
|
|
242
|
+
deps.settings.publicBaseUrl!,
|
|
243
|
+
).toString(),
|
|
244
|
+
}
|
|
245
|
+
: ManagedSignInMutationResponse.parse(prior.result),
|
|
246
|
+
);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return mutationError(context, error);
|
|
249
|
+
}
|
|
250
|
+
// Per-human, cross-replica admission before password hashing or creating
|
|
251
|
+
// OAuth state. Successful committed-result replay does not spend a slot.
|
|
252
|
+
const [limit] = await rows<{ count: number }>(
|
|
253
|
+
deps,
|
|
254
|
+
sql`
|
|
255
|
+
insert into auth_rate_limits(id,key,count,last_request)
|
|
256
|
+
values(${`sign-in-methods:${actor.authUserId}`},${`sign-in-methods:${actor.authUserId}`},1,(extract(epoch from clock_timestamp())*1000)::bigint)
|
|
257
|
+
on conflict(key) do update set
|
|
258
|
+
count=case when auth_rate_limits.last_request < (extract(epoch from clock_timestamp())*1000)::bigint-60000 then 1 else least(auth_rate_limits.count+1,11) end,
|
|
259
|
+
last_request=case when auth_rate_limits.last_request < (extract(epoch from clock_timestamp())*1000)::bigint-60000 then (extract(epoch from clock_timestamp())*1000)::bigint else auth_rate_limits.last_request end
|
|
260
|
+
returning count`,
|
|
261
|
+
);
|
|
262
|
+
if (!limit || limit.count > 10)
|
|
263
|
+
return context.json(
|
|
264
|
+
{
|
|
265
|
+
code: "SIGN_IN_METHOD_RATE_LIMITED",
|
|
266
|
+
message: "Too many sign-in method attempts; try again in one minute",
|
|
267
|
+
},
|
|
268
|
+
429,
|
|
269
|
+
);
|
|
270
|
+
if ("provider" in body) {
|
|
271
|
+
request.provider = body.provider;
|
|
272
|
+
if (kind === "connect" && !configured(deps, body.provider))
|
|
273
|
+
throw new HTTPException(409, { message: "Sign-in provider is not configured" });
|
|
274
|
+
} else {
|
|
275
|
+
const [credential] = await rows<{ password: string | null }>(
|
|
276
|
+
deps,
|
|
277
|
+
sql`select password from auth_identities where user_id=${actor.authUserId} and provider_id='credential'`,
|
|
278
|
+
);
|
|
279
|
+
if (
|
|
280
|
+
credential?.password &&
|
|
281
|
+
(!body.currentPassword ||
|
|
282
|
+
!(await verifyManagedAuthPassword(body.currentPassword, credential.password)))
|
|
283
|
+
) {
|
|
284
|
+
return context.json(
|
|
285
|
+
{
|
|
286
|
+
code: "SIGN_IN_METHOD_CURRENT_PASSWORD_REQUIRED",
|
|
287
|
+
message: "Enter your current password",
|
|
288
|
+
},
|
|
289
|
+
403,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
request.expectedPasswordHash = credential?.password ?? null;
|
|
293
|
+
request.passwordHash = await hashManagedAuthPassword(body.newPassword);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await rows(
|
|
297
|
+
deps,
|
|
298
|
+
sql`select mutate_managed_sign_in_method(${actor.authUserId},${actor.authSessionId},${JSON.stringify(request)}::jsonb)`,
|
|
299
|
+
);
|
|
300
|
+
if (kind === "connect" && "provider" in body) {
|
|
301
|
+
const headers = await managedAuthSelectedProofHeaders(deps.managedAuth!, user.token);
|
|
302
|
+
headers.set("origin", new URL(deps.settings.publicBaseUrl!).origin);
|
|
303
|
+
const callbackURL = new URL(
|
|
304
|
+
"/settings/security?signInMethod=connected",
|
|
305
|
+
deps.settings.publicBaseUrl!,
|
|
306
|
+
).toString();
|
|
307
|
+
const errorCallbackURL = new URL(
|
|
308
|
+
"/settings/security?signInMethod=error",
|
|
309
|
+
deps.settings.publicBaseUrl!,
|
|
310
|
+
).toString();
|
|
311
|
+
const result = await deps.managedAuth!.api.linkSocialAccount({
|
|
312
|
+
headers,
|
|
313
|
+
returnHeaders: true,
|
|
314
|
+
body: {
|
|
315
|
+
provider: body.provider,
|
|
316
|
+
callbackURL,
|
|
317
|
+
errorCallbackURL,
|
|
318
|
+
disableRedirect: true,
|
|
319
|
+
additionalData: {
|
|
320
|
+
opengeniSignInMethod: { intentId: body.operationId, provider: body.provider },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
for (const cookie of result.headers.getSetCookie())
|
|
325
|
+
context.header("set-cookie", cookie, { append: true });
|
|
326
|
+
return context.json({ url: result.response.url });
|
|
327
|
+
}
|
|
328
|
+
if (actorFence) markManagedAuthRequestActorTransitionApplied(context.req.raw);
|
|
329
|
+
return context.json({
|
|
330
|
+
reauthenticationRequired: true,
|
|
331
|
+
notification: await deliverManagedSignInNotification(
|
|
332
|
+
deps.db,
|
|
333
|
+
deps.managedEmailTransport,
|
|
334
|
+
body.operationId,
|
|
335
|
+
),
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return mutationError(context, error);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Intercept only database-backed, product-issued explicit connect states. */
|
|
345
|
+
export async function handleManagedSignInConnectCallback(
|
|
346
|
+
context: Context,
|
|
347
|
+
deps: ApiRouteDeps,
|
|
348
|
+
provider: "google" | "github",
|
|
349
|
+
): Promise<Response | null> {
|
|
350
|
+
const state = context.req.query("state");
|
|
351
|
+
if (!state || !deps.managedAuth) return null;
|
|
352
|
+
const verification = await (
|
|
353
|
+
await deps.managedAuth.$context
|
|
354
|
+
).internalAdapter.findVerificationValue(state);
|
|
355
|
+
if (!verification?.value) return null;
|
|
356
|
+
let rawState: unknown;
|
|
357
|
+
try {
|
|
358
|
+
rawState = JSON.parse(verification.value);
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
if (!rawState || typeof rawState !== "object" || !("opengeniSignInMethod" in rawState))
|
|
363
|
+
return null;
|
|
364
|
+
const parsed = ConnectState.safeParse(rawState);
|
|
365
|
+
if (!parsed.success)
|
|
366
|
+
throw new HTTPException(403, { message: "Invalid sign-in method OAuth proof" });
|
|
367
|
+
const value = parsed.data,
|
|
368
|
+
proof = value.opengeniSignInMethod;
|
|
369
|
+
if (
|
|
370
|
+
proof.provider !== provider ||
|
|
371
|
+
value.callbackURL !==
|
|
372
|
+
new URL(
|
|
373
|
+
"/settings/security?signInMethod=connected",
|
|
374
|
+
deps.settings.publicBaseUrl!,
|
|
375
|
+
).toString() ||
|
|
376
|
+
value.errorURL !==
|
|
377
|
+
new URL("/settings/security?signInMethod=error", deps.settings.publicBaseUrl!).toString()
|
|
378
|
+
)
|
|
379
|
+
throw new HTTPException(403);
|
|
380
|
+
const [intent] = await rows<{ value: string }>(
|
|
381
|
+
deps,
|
|
382
|
+
sql`select value from auth_verifications where identifier=${`managed-sign-in:${proof.intentId}`} and expires_at>clock_timestamp()`,
|
|
383
|
+
);
|
|
384
|
+
if (!intent) throw new HTTPException(403, { message: "Sign-in method connection expired" });
|
|
385
|
+
const request = ConnectIntent.parse(JSON.parse(intent.value));
|
|
386
|
+
const authority = getCookie(context, MANAGED_AUTH_SESSION_SET_COOKIE);
|
|
387
|
+
if (!request.actorFence) {
|
|
388
|
+
const ambient = await deps.managedAuthSessionAdapter?.resolveAmbientSession(
|
|
389
|
+
context.req.raw.headers,
|
|
390
|
+
);
|
|
391
|
+
if (
|
|
392
|
+
!ambient ||
|
|
393
|
+
ambient.session.id !== request.authSessionId ||
|
|
394
|
+
ambient.user.id !== request.authUserId
|
|
395
|
+
) {
|
|
396
|
+
return context.redirect(
|
|
397
|
+
new URL(
|
|
398
|
+
"/settings/security?signInMethod=error&error=SIGN_IN_METHOD_IDENTITY_CHANGED",
|
|
399
|
+
deps.settings.publicBaseUrl!,
|
|
400
|
+
).toString(),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (
|
|
405
|
+
request.actorFence &&
|
|
406
|
+
(!authority || managedAuthSha256(authority) !== request.actorFence.authorityHash)
|
|
407
|
+
)
|
|
408
|
+
throw new HTTPException(403);
|
|
409
|
+
if (value.link?.userId !== request.authUserId || value.link?.email === undefined)
|
|
410
|
+
throw new HTTPException(403);
|
|
411
|
+
const isolated = await isolatedManagedAuthOAuthCallbackRequest(deps.managedAuth, context.req.raw);
|
|
412
|
+
const response = await runManagedSignInConnect(proof.intentId, () =>
|
|
413
|
+
deps.managedAuth!.handler(isolated.request),
|
|
414
|
+
);
|
|
415
|
+
const [linked] = await rows<{ email: string }>(
|
|
416
|
+
deps,
|
|
417
|
+
sql`select u.email from auth_identities a join auth_users u on u.id=a.user_id where a.managed_link_intent_id=${proof.intentId}`,
|
|
418
|
+
);
|
|
419
|
+
if (linked)
|
|
420
|
+
await deliverManagedSignInNotification(deps.db, deps.managedEmailTransport, proof.intentId);
|
|
421
|
+
if (response.status >= 400 || !linked) {
|
|
422
|
+
return context.redirect(
|
|
423
|
+
new URL(
|
|
424
|
+
"/settings/security?signInMethod=error&error=SIGN_IN_METHOD_CONNECT_FAILED",
|
|
425
|
+
deps.settings.publicBaseUrl!,
|
|
426
|
+
).toString(),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return scrubManagedAuthProviderResponse(response, {
|
|
430
|
+
replacementCookies: [],
|
|
431
|
+
preserveCookieNames: [isolated.stateCookieName],
|
|
432
|
+
});
|
|
433
|
+
}
|
package/src/routes/plugins.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
getConnectionMetadata,
|
|
49
49
|
getInstalledPluginPackage,
|
|
50
50
|
getPluginPackageUninstallPreview,
|
|
51
|
+
PluginUninstallPreviewChangedError,
|
|
51
52
|
integrationBindingKey,
|
|
52
53
|
installApiIntegration,
|
|
53
54
|
installPluginMcpReference,
|
|
@@ -349,9 +350,14 @@ export function registerPluginRoutes(
|
|
|
349
350
|
|
|
350
351
|
app.get("/v1/workspaces/:workspaceId/plugins/:pluginKey/uninstall-preview", async (c) => {
|
|
351
352
|
const workspaceId = c.req.param("workspaceId");
|
|
352
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
353
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
353
354
|
const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
|
|
354
|
-
const preview = await getPluginPackageUninstallPreview(
|
|
355
|
+
const preview = await getPluginPackageUninstallPreview(
|
|
356
|
+
deps.db,
|
|
357
|
+
workspaceId,
|
|
358
|
+
pluginKey,
|
|
359
|
+
grant.subjectId,
|
|
360
|
+
);
|
|
355
361
|
return c.json(PluginUninstallPreview.parse({ pluginKey, ...preview }));
|
|
356
362
|
});
|
|
357
363
|
|
|
@@ -378,11 +384,33 @@ export function registerPluginRoutes(
|
|
|
378
384
|
subjectId: grant.subjectId,
|
|
379
385
|
pluginKey,
|
|
380
386
|
expectedInstallationVersion: payload.expectedInstallationVersion,
|
|
387
|
+
...(payload.expectedPreviewToken
|
|
388
|
+
? { expectedPreviewToken: payload.expectedPreviewToken }
|
|
389
|
+
: {}),
|
|
381
390
|
idempotencyKey: payload.idempotencyKey,
|
|
382
391
|
})),
|
|
383
392
|
}),
|
|
384
393
|
);
|
|
385
394
|
} catch (error) {
|
|
395
|
+
if (error instanceof PluginUninstallPreviewChangedError) {
|
|
396
|
+
const preview = error.preview ?? {
|
|
397
|
+
pluginKey,
|
|
398
|
+
...(await getPluginPackageUninstallPreview(
|
|
399
|
+
deps.db,
|
|
400
|
+
workspaceId,
|
|
401
|
+
pluginKey,
|
|
402
|
+
grant.subjectId,
|
|
403
|
+
)),
|
|
404
|
+
};
|
|
405
|
+
return c.json(
|
|
406
|
+
{
|
|
407
|
+
code: error.code,
|
|
408
|
+
message: error.message,
|
|
409
|
+
preview: PluginUninstallPreview.parse(preview),
|
|
410
|
+
},
|
|
411
|
+
409,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
386
414
|
throw pluginMutationHttpError(error);
|
|
387
415
|
}
|
|
388
416
|
});
|