@opengeni/api-router 2.11.3 → 2.12.3-canary.0
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-5X3XM6CE.js → chunk-4AQMKVQM.js} +17727 -14427
- package/dist/chunk-4AQMKVQM.js.map +1 -0
- package/dist/connection-ownership.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +4 -0
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +7 -0
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +153 -0
- package/dist/integrations/google-drive.d.ts +4 -0
- package/dist/integrations/oauth-client.d.ts +4 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github.d.ts +2 -0
- package/dist/integrations/provider-oauth.d.ts +11 -1
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/social-oauth.d.ts +2 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/host-mcp-bindings.d.ts +5 -0
- package/dist/sandbox/rematerialize.d.ts +1 -1
- package/dist/workspace-tool-gateway.d.ts +1 -0
- package/package.json +19 -19
- package/src/app.ts +22 -2
- package/src/connection-ownership.ts +14 -1
- package/src/index.ts +2 -0
- package/src/integrations/atlassian.ts +143 -39
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +222 -40
- package/src/integrations/github-app-connect.ts +389 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +97 -0
- package/src/integrations/google-drive.ts +152 -47
- package/src/integrations/oauth-client.ts +180 -84
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github.ts +146 -52
- package/src/integrations/provider-oauth.ts +132 -21
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/social-oauth.ts +142 -1
- package/src/mcp/documents.ts +1 -1
- package/src/mcp/server.ts +64 -17
- package/src/routes/api-integrations.ts +74 -64
- package/src/routes/connect.ts +1869 -0
- package/src/routes/connections.ts +263 -221
- package/src/routes/documents.ts +2 -2
- package/src/routes/external-identity-links.ts +200 -0
- package/src/routes/github.ts +39 -64
- package/src/routes/host-mcp-bindings.ts +122 -0
- package/src/routes/organization-memberships.ts +23 -0
- package/src/routes/organization-sessions.ts +2 -2
- package/src/routes/personal-github.ts +8 -1
- package/src/routes/pr-review-github.ts +30 -3
- package/src/routes/scheduled-tasks.ts +29 -4
- package/src/routes/sessions.ts +61 -32
- package/src/routes/social.ts +5 -1
- package/src/routes/supergrok.ts +90 -3
- package/src/routes/workspaces.ts +16 -0
- package/src/sandbox/rematerialize.ts +33 -1
- package/src/workspace-tool-gateway.ts +136 -24
- package/dist/chunk-5X3XM6CE.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeReturnPath } from "./oauth-return-path";
|
|
1
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
3
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
4
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
@@ -8,22 +9,23 @@ import {
|
|
|
8
9
|
type ConnectionOwnership,
|
|
9
10
|
type OAuthStartRequest,
|
|
10
11
|
} from "@opengeni/contracts";
|
|
11
|
-
import {
|
|
12
|
+
import { requireEnvironmentEncryption } from "@opengeni/core";
|
|
13
|
+
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
12
14
|
import type { Observability } from "@opengeni/observability";
|
|
13
15
|
import {
|
|
14
16
|
consumeIntegrationOAuthStateNonce,
|
|
17
|
+
claimConnectOperation,
|
|
18
|
+
finishConnectOperation,
|
|
19
|
+
getConnectAttempt,
|
|
15
20
|
createConnection,
|
|
16
21
|
decryptEnvironmentValue,
|
|
17
22
|
encryptEnvironmentValue,
|
|
18
|
-
findActiveWorkspaceApiKeyById,
|
|
19
23
|
getConnectionMetadata,
|
|
20
24
|
getGlobalCatalogOAuthProfile,
|
|
21
|
-
getWorkspaceGrant,
|
|
22
25
|
listConnectionsMetadata,
|
|
23
26
|
loadIntegrationOAuthClient,
|
|
24
27
|
normalizeBearerScheme,
|
|
25
28
|
replaceIntegrationOAuthClientIfCurrent,
|
|
26
|
-
resolveNamedManagedPersonalWorkspaceGrant,
|
|
27
29
|
storeIntegrationOAuthClient,
|
|
28
30
|
updateConnection,
|
|
29
31
|
withDatabaseStatementTimeout,
|
|
@@ -58,6 +60,7 @@ import {
|
|
|
58
60
|
PERSONAL_OWNER_VERIFIED_STATE_CLAIM,
|
|
59
61
|
} from "../connection-ownership";
|
|
60
62
|
import { ApiHttpError } from "../http/api-error";
|
|
63
|
+
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
61
64
|
import {
|
|
62
65
|
DEFAULT_OAUTH_PROFILE,
|
|
63
66
|
DEPLOYMENT_MANAGED_CLIENTS,
|
|
@@ -92,6 +95,8 @@ type OAuthClientDeps = {
|
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
export type OAuthStartContext = {
|
|
98
|
+
connectAttemptId?: string;
|
|
99
|
+
externalContinuation?: ExternalActorContinuation;
|
|
95
100
|
accountId: string;
|
|
96
101
|
workspaceId: string;
|
|
97
102
|
subjectId: string;
|
|
@@ -107,6 +112,7 @@ export type OAuthStartContext = {
|
|
|
107
112
|
|
|
108
113
|
export type OAuthCallbackResult = {
|
|
109
114
|
redirectTo: string;
|
|
115
|
+
exactReturn?: boolean;
|
|
110
116
|
};
|
|
111
117
|
|
|
112
118
|
type WwwAuthenticateChallenge = McpOAuthChallenge;
|
|
@@ -123,6 +129,9 @@ type OAuthClientRegistration = {
|
|
|
123
129
|
};
|
|
124
130
|
|
|
125
131
|
type OAuthStatePayload = {
|
|
132
|
+
connectAttemptId?: string;
|
|
133
|
+
externalContinuation?: ExternalActorContinuation;
|
|
134
|
+
returnUrl?: string;
|
|
126
135
|
accountId: string;
|
|
127
136
|
workspaceId: string;
|
|
128
137
|
subjectId: string;
|
|
@@ -328,6 +337,26 @@ async function startMcpOAuthWithinDeadline(
|
|
|
328
337
|
deadline: OAuthStartDeadline,
|
|
329
338
|
): Promise<OAuthStartResponse> {
|
|
330
339
|
const { db, settings } = deps;
|
|
340
|
+
const externalContinuation = context.externalContinuation
|
|
341
|
+
? ExternalActorContinuation.parse(context.externalContinuation)
|
|
342
|
+
: undefined;
|
|
343
|
+
const returnUrl =
|
|
344
|
+
context.payload.returnUrl !== undefined
|
|
345
|
+
? exactExternalReturnUrl(context.payload.returnUrl)
|
|
346
|
+
: undefined;
|
|
347
|
+
if (
|
|
348
|
+
(externalContinuation && !returnUrl) ||
|
|
349
|
+
(returnUrl && !externalContinuation && !context.connectAttemptId)
|
|
350
|
+
)
|
|
351
|
+
throw new HTTPException(422, {
|
|
352
|
+
message: "external MCP OAuth requires verified actor authority and a host returnUrl",
|
|
353
|
+
});
|
|
354
|
+
if (
|
|
355
|
+
externalContinuation &&
|
|
356
|
+
(externalContinuation.actor.accountId !== context.accountId ||
|
|
357
|
+
externalContinuation.actor.effectiveSubjectId !== context.subjectId)
|
|
358
|
+
)
|
|
359
|
+
throw new HTTPException(403, { message: "external OAuth actor mismatch" });
|
|
331
360
|
const mcpUrl = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
|
|
332
361
|
const urlProfile = builtInOAuthProfileFor({ mcpUrl });
|
|
333
362
|
const providerDomain =
|
|
@@ -414,6 +443,7 @@ async function startMcpOAuthWithinDeadline(
|
|
|
414
443
|
);
|
|
415
444
|
const key = requireEnvironmentEncryption(settings);
|
|
416
445
|
const state = createSignedState(requireIntegrationsStateSecret(settings), {
|
|
446
|
+
...(context.connectAttemptId ? { connectAttemptId: context.connectAttemptId } : {}),
|
|
417
447
|
accountId: context.accountId,
|
|
418
448
|
workspaceId: context.workspaceId,
|
|
419
449
|
subjectId: context.subjectId,
|
|
@@ -421,6 +451,15 @@ async function startMcpOAuthWithinDeadline(
|
|
|
421
451
|
// Signed record that a live principal was checked; the callback has no
|
|
422
452
|
// principal of its own and enforces exactly this decision.
|
|
423
453
|
[PERSONAL_OWNER_VERIFIED_STATE_CLAIM]: context.personalOwnershipAllowed,
|
|
454
|
+
...(externalContinuation
|
|
455
|
+
? {
|
|
456
|
+
encryptedExternalContinuation: encryptEnvironmentValue(
|
|
457
|
+
requireEnvironmentEncryption(settings),
|
|
458
|
+
JSON.stringify(externalContinuation),
|
|
459
|
+
),
|
|
460
|
+
}
|
|
461
|
+
: {}),
|
|
462
|
+
...(returnUrl ? { returnUrl } : {}),
|
|
424
463
|
providerDomain,
|
|
425
464
|
mcpUrl,
|
|
426
465
|
resource,
|
|
@@ -588,6 +627,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
588
627
|
): Promise<OAuthCallbackResult> {
|
|
589
628
|
const { db, settings, observability } = deps;
|
|
590
629
|
let state: OAuthStatePayload | null = null;
|
|
630
|
+
let connectOperation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
|
|
591
631
|
if (!input.state) {
|
|
592
632
|
const error = new OAuthCallbackStageError(
|
|
593
633
|
"state_verify",
|
|
@@ -604,6 +644,27 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
604
644
|
}
|
|
605
645
|
try {
|
|
606
646
|
state = readOAuthState(input.state, settings);
|
|
647
|
+
if (state.connectAttemptId) {
|
|
648
|
+
const stored = await getConnectAttempt(db, state, state.connectAttemptId);
|
|
649
|
+
if (
|
|
650
|
+
!["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId) ||
|
|
651
|
+
stored.attempt.ownership !== state.ownership ||
|
|
652
|
+
stored.returnUrl !== state.returnUrl
|
|
653
|
+
)
|
|
654
|
+
throw new HTTPException(403, { message: "OAuth attempt mismatch" });
|
|
655
|
+
connectOperation = {
|
|
656
|
+
attemptId: state.connectAttemptId,
|
|
657
|
+
operationId: `oauth:${state.nonce}`,
|
|
658
|
+
inputDigest: createHash("sha256").update(input.state).digest("hex"),
|
|
659
|
+
};
|
|
660
|
+
const claim = await claimConnectOperation(db, state, {
|
|
661
|
+
...connectOperation,
|
|
662
|
+
expectedRevision: stored.attempt.revision,
|
|
663
|
+
authorize: (tx, _attempt, origin) =>
|
|
664
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
665
|
+
});
|
|
666
|
+
if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
667
|
+
}
|
|
607
668
|
// Fence state minted by an older deployment too: a rolling update must not
|
|
608
669
|
// let a still-valid callback persist an ownership the target's profile no
|
|
609
670
|
// longer allows (workspace-owned Gmail or hosted Slack, for example).
|
|
@@ -627,12 +688,28 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
627
688
|
);
|
|
628
689
|
}
|
|
629
690
|
if (!input.code) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
691
|
+
if (connectOperation) {
|
|
692
|
+
await finishConnectOperation(db, state, {
|
|
693
|
+
...connectOperation,
|
|
694
|
+
authorize: (tx, _attempt, origin) =>
|
|
695
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
696
|
+
commit: async (_tx, current) => ({
|
|
697
|
+
...current,
|
|
698
|
+
revision: current.revision + 1,
|
|
699
|
+
state: "failed",
|
|
700
|
+
nextAction: { type: "none" },
|
|
701
|
+
error: {
|
|
702
|
+
code: "missing_code",
|
|
703
|
+
message: "Authorization was not completed. Start a new connection attempt.",
|
|
704
|
+
retryable: false,
|
|
705
|
+
},
|
|
706
|
+
}),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return callbackStateResult(state, "error", {
|
|
710
|
+
stage: "state_verify",
|
|
711
|
+
reason: "missing_code",
|
|
712
|
+
});
|
|
636
713
|
}
|
|
637
714
|
const consumed = await runCallbackDatabaseStage(
|
|
638
715
|
deadline,
|
|
@@ -673,12 +750,10 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
673
750
|
? error
|
|
674
751
|
: new OAuthCallbackStageError("state_verify", "state_invalid", error);
|
|
675
752
|
logOAuthCallbackFailure(observability, staged, state);
|
|
676
|
-
return {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}),
|
|
681
|
-
};
|
|
753
|
+
return callbackStateResult(state, "error", {
|
|
754
|
+
stage: staged.stage,
|
|
755
|
+
reason: staged.reason,
|
|
756
|
+
});
|
|
682
757
|
}
|
|
683
758
|
|
|
684
759
|
const ownerSubjectId = state.ownership === "personal" ? state.subjectId : null;
|
|
@@ -735,7 +810,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
735
810
|
...(verification.tools ? { mcpTools: verification.tools } : {}),
|
|
736
811
|
};
|
|
737
812
|
const credentialEncrypted = encryptEnvironmentValue(key, JSON.stringify(credential));
|
|
738
|
-
const
|
|
813
|
+
const persist = async (scopedDb: Database) => {
|
|
739
814
|
await requireOAuthCallbackGrant(scopedDb, state!);
|
|
740
815
|
return state!.connectionId
|
|
741
816
|
? await updateConnection(scopedDb, {
|
|
@@ -765,7 +840,41 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
765
840
|
metadata,
|
|
766
841
|
createdBySubjectId: state.subjectId,
|
|
767
842
|
});
|
|
768
|
-
}
|
|
843
|
+
};
|
|
844
|
+
if (connectOperation) {
|
|
845
|
+
await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
|
|
846
|
+
finishConnectOperation(scopedDb, state!, {
|
|
847
|
+
...connectOperation!,
|
|
848
|
+
authorize: (tx, _attempt, origin) =>
|
|
849
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
850
|
+
commit: async (tx, current) => {
|
|
851
|
+
const connection = await persist(tx);
|
|
852
|
+
if (!connection)
|
|
853
|
+
throw new HTTPException(409, {
|
|
854
|
+
message: "connection changed during OAuth reconnect",
|
|
855
|
+
});
|
|
856
|
+
return {
|
|
857
|
+
...current,
|
|
858
|
+
revision: current.revision + 1,
|
|
859
|
+
state: "complete",
|
|
860
|
+
completionRequirement: "connection",
|
|
861
|
+
credentialsCommitted: true,
|
|
862
|
+
nextAction: { type: "none" },
|
|
863
|
+
account: {
|
|
864
|
+
id: connection.id,
|
|
865
|
+
version: connection.version,
|
|
866
|
+
providerId: current.providerId,
|
|
867
|
+
label: state!.providerDomain,
|
|
868
|
+
ownership: current.ownership,
|
|
869
|
+
status: "connected",
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
}),
|
|
874
|
+
);
|
|
875
|
+
return { redirectTo: state.returnUrl!, exactReturn: true };
|
|
876
|
+
}
|
|
877
|
+
const connection = await runCallbackDatabaseStage(deadline, "persist", db, persist);
|
|
769
878
|
if (!connection) {
|
|
770
879
|
throw new HTTPException(409, {
|
|
771
880
|
message: "connection changed during OAuth reconnect; start again",
|
|
@@ -775,29 +884,49 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
775
884
|
// the enable connectionRef straight from the redirect, without a listConnections
|
|
776
885
|
// round-trip that could fail (transient, or a grant lacking connections:read)
|
|
777
886
|
// and leave the connection created but the capability un-enabled.
|
|
778
|
-
return {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}),
|
|
785
|
-
};
|
|
887
|
+
return callbackStateResult(state, "success", {
|
|
888
|
+
connectionId: connection.id,
|
|
889
|
+
providerDomain: connection.providerDomain,
|
|
890
|
+
ownership: state.ownership,
|
|
891
|
+
...(verification.metadata.status === "failed" ? { verification: "failed" } : {}),
|
|
892
|
+
});
|
|
786
893
|
} catch (error) {
|
|
787
894
|
const staged =
|
|
788
895
|
error instanceof OAuthCallbackStageError
|
|
789
896
|
? error
|
|
790
897
|
: new OAuthCallbackStageError("persist", "persist_failed", error);
|
|
791
898
|
logOAuthCallbackFailure(observability, staged, state);
|
|
792
|
-
return {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}),
|
|
797
|
-
};
|
|
899
|
+
return callbackStateResult(state, "error", {
|
|
900
|
+
stage: staged.stage,
|
|
901
|
+
reason: staged.reason,
|
|
902
|
+
});
|
|
798
903
|
}
|
|
799
904
|
}
|
|
800
905
|
|
|
906
|
+
function exactExternalReturnUrl(raw: string): string {
|
|
907
|
+
if (!raw || raw.length > 4096 || /[\u0000-\u0020\u007f]/.test(raw))
|
|
908
|
+
throw new HTTPException(422, { message: "invalid external OAuth returnUrl" });
|
|
909
|
+
let url: URL;
|
|
910
|
+
try {
|
|
911
|
+
url = new URL(raw);
|
|
912
|
+
} catch {
|
|
913
|
+
throw new HTTPException(422, { message: "invalid external OAuth returnUrl" });
|
|
914
|
+
}
|
|
915
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
916
|
+
throw new HTTPException(422, { message: "invalid external OAuth returnUrl" });
|
|
917
|
+
return raw;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function callbackStateResult(
|
|
921
|
+
state: OAuthStatePayload | null,
|
|
922
|
+
status: Parameters<typeof callbackReturnPath>[1],
|
|
923
|
+
details: Parameters<typeof callbackReturnPath>[2],
|
|
924
|
+
): OAuthCallbackResult {
|
|
925
|
+
if ((state?.externalContinuation || state?.connectAttemptId) && state.returnUrl)
|
|
926
|
+
return { redirectTo: state.returnUrl, exactReturn: true };
|
|
927
|
+
return { redirectTo: callbackReturnPath(state?.returnPath ?? "/integrations", status, details) };
|
|
928
|
+
}
|
|
929
|
+
|
|
801
930
|
export function integrationBaseUrl(publicBaseUrl: string | undefined, requestUrl: string): string {
|
|
802
931
|
return (publicBaseUrl ?? new URL(requestUrl).origin).replace(/\/+$/, "");
|
|
803
932
|
}
|
|
@@ -813,41 +942,7 @@ export function requireIntegrationsStateSecret(settings: Settings): string {
|
|
|
813
942
|
}
|
|
814
943
|
|
|
815
944
|
async function requireOAuthCallbackGrant(db: Database, state: OAuthStatePayload): Promise<void> {
|
|
816
|
-
|
|
817
|
-
const grant =
|
|
818
|
-
membershipGrant?.accountId === state.accountId
|
|
819
|
-
? membershipGrant
|
|
820
|
-
: state.personalOwnerVerified
|
|
821
|
-
? await resolveNamedManagedPersonalWorkspaceGrant(db, state)
|
|
822
|
-
: await apiKeyCallbackGrant(db, state);
|
|
823
|
-
if (
|
|
824
|
-
!grant ||
|
|
825
|
-
grant.accountId !== state.accountId ||
|
|
826
|
-
!hasPermission(grant.permissions, "connections:write")
|
|
827
|
-
) {
|
|
828
|
-
throw new HTTPException(403, {
|
|
829
|
-
message: "OAuth subject no longer has permission to write this workspace connection",
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
async function apiKeyCallbackGrant(
|
|
835
|
-
db: Database,
|
|
836
|
-
state: OAuthStatePayload,
|
|
837
|
-
): Promise<{ accountId: string; permissions: Parameters<typeof hasPermission>[0] } | null> {
|
|
838
|
-
const prefix = "api_key:";
|
|
839
|
-
if (!state.subjectId.startsWith(prefix)) {
|
|
840
|
-
return null;
|
|
841
|
-
}
|
|
842
|
-
const apiKey = await findActiveWorkspaceApiKeyById(db, {
|
|
843
|
-
accountId: state.accountId,
|
|
844
|
-
workspaceId: state.workspaceId,
|
|
845
|
-
apiKeyId: state.subjectId.slice(prefix.length),
|
|
846
|
-
});
|
|
847
|
-
if (!apiKey) {
|
|
848
|
-
return null;
|
|
849
|
-
}
|
|
850
|
-
return { accountId: apiKey.accountId, permissions: apiKey.permissions };
|
|
945
|
+
await requireConnectOwnerAuthority(db, state);
|
|
851
946
|
}
|
|
852
947
|
|
|
853
948
|
/** The hosted-Slack profile's origin pins, kept exported for its tests. */
|
|
@@ -1451,6 +1546,24 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1451
1546
|
accountId: requiredString(payload.accountId, "state.accountId"),
|
|
1452
1547
|
workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
|
|
1453
1548
|
subjectId: requiredString(payload.subjectId, "state.subjectId"),
|
|
1549
|
+
...(stringValue(payload.encryptedExternalContinuation)
|
|
1550
|
+
? {
|
|
1551
|
+
externalContinuation: ExternalActorContinuation.parse(
|
|
1552
|
+
JSON.parse(
|
|
1553
|
+
decryptEnvironmentValue(
|
|
1554
|
+
requireEnvironmentEncryption(settings),
|
|
1555
|
+
requiredString(payload.encryptedExternalContinuation, "state.externalContinuation"),
|
|
1556
|
+
),
|
|
1557
|
+
),
|
|
1558
|
+
),
|
|
1559
|
+
}
|
|
1560
|
+
: {}),
|
|
1561
|
+
...(payload.returnUrl !== undefined
|
|
1562
|
+
? { returnUrl: exactExternalReturnUrl(requiredString(payload.returnUrl, "state.returnUrl")) }
|
|
1563
|
+
: {}),
|
|
1564
|
+
...(stringValue(payload.connectAttemptId)
|
|
1565
|
+
? { connectAttemptId: requiredString(payload.connectAttemptId, "state.connectAttemptId") }
|
|
1566
|
+
: {}),
|
|
1454
1567
|
// OAuth states minted before ownership was explicit were always personal.
|
|
1455
1568
|
// Preserve that meaning for in-flight reconnects during a rolling deploy.
|
|
1456
1569
|
ownership: connectionOwnership(payload.ownership) ?? "personal",
|
|
@@ -2085,23 +2198,6 @@ function oauthEndpointUrl(rawUrl: string, settings: Settings, label: string): st
|
|
|
2085
2198
|
}
|
|
2086
2199
|
}
|
|
2087
2200
|
|
|
2088
|
-
function safeReturnPath(value: string): string {
|
|
2089
|
-
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
2090
|
-
throw new HTTPException(400, {
|
|
2091
|
-
message: "OAuth returnPath must be a relative path",
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
const parsed = new URL(value, "https://opengeni.local");
|
|
2095
|
-
// `..` segments can normalize back into a `//host` prefix, which browsers
|
|
2096
|
-
// resolve as a protocol-relative absolute URL. Reject the NORMALIZED path.
|
|
2097
|
-
if (parsed.origin !== "https://opengeni.local" || parsed.pathname.startsWith("//")) {
|
|
2098
|
-
throw new HTTPException(400, {
|
|
2099
|
-
message: "OAuth returnPath must be a relative path",
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
2201
|
async function fetchOAuth(
|
|
2106
2202
|
rawUrl: string,
|
|
2107
2203
|
settings: Settings,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
|
|
3
|
+
export function safeReturnPath(value: string): string {
|
|
4
|
+
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
5
|
+
throw new HTTPException(400, {
|
|
6
|
+
message: "OAuth returnPath must be a relative path",
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
const parsed = new URL(value, "https://opengeni.local");
|
|
10
|
+
// `..` segments can normalize back into a `//host` prefix, which browsers
|
|
11
|
+
// resolve as a protocol-relative absolute URL. Reject the NORMALIZED path.
|
|
12
|
+
if (parsed.origin !== "https://opengeni.local" || parsed.pathname.startsWith("//")) {
|
|
13
|
+
throw new HTTPException(400, {
|
|
14
|
+
message: "OAuth returnPath must be a relative path",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
18
|
+
}
|