@agent-native/dispatch 0.27.8 → 0.27.10
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/actions/create_workspace_app_embed_session.d.ts.map +1 -1
- package/dist/actions/create_workspace_app_embed_session.js +6 -3
- package/dist/actions/create_workspace_app_embed_session.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +5 -0
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +144 -47
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/create_workspace_app_embed_session.spec.ts +110 -0
- package/src/actions/create_workspace_app_embed_session.ts +6 -5
- package/src/server/lib/app-creation-store.ts +5 -0
- package/src/server/lib/mcp-gateway.spec.ts +94 -17
- package/src/server/lib/mcp-gateway.ts +185 -56
|
@@ -942,13 +942,18 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
942
942
|
expect(mocks.managerConstructor).toHaveBeenCalled();
|
|
943
943
|
});
|
|
944
944
|
|
|
945
|
-
it("
|
|
945
|
+
it("excludes path-mounted apps from SSO while retaining canonical apps", async () => {
|
|
946
946
|
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
947
|
-
mocks.discoverAgents.mockResolvedValue([
|
|
947
|
+
mocks.discoverAgents.mockResolvedValue([
|
|
948
|
+
{
|
|
949
|
+
...analyticsAgent,
|
|
950
|
+
url: "https://analytics.agent-native.com",
|
|
951
|
+
},
|
|
952
|
+
]);
|
|
948
953
|
mocks.managerCallTool.mockResolvedValueOnce({
|
|
949
954
|
structuredContent: {
|
|
950
955
|
startUrl:
|
|
951
|
-
"https://agent-
|
|
956
|
+
"https://analytics.agent-native.com/_agent-native/embed/start?ticket=remote",
|
|
952
957
|
},
|
|
953
958
|
});
|
|
954
959
|
mocks.listWorkspaceApps.mockResolvedValue([
|
|
@@ -965,6 +970,20 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
965
970
|
},
|
|
966
971
|
]);
|
|
967
972
|
|
|
973
|
+
await expect(
|
|
974
|
+
runWithRequestContext(
|
|
975
|
+
{
|
|
976
|
+
userEmail: "owner@example.test",
|
|
977
|
+
requestOrigin: "https://dispatch.agent-native.com",
|
|
978
|
+
},
|
|
979
|
+
() =>
|
|
980
|
+
createWorkspaceSsoEmbedSession({
|
|
981
|
+
app: "atlas",
|
|
982
|
+
path: "/",
|
|
983
|
+
}),
|
|
984
|
+
),
|
|
985
|
+
).rejects.toThrow(/not registered/);
|
|
986
|
+
|
|
968
987
|
const result = await runWithRequestContext(
|
|
969
988
|
{
|
|
970
989
|
userEmail: "owner@example.test",
|
|
@@ -972,23 +991,13 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
972
991
|
},
|
|
973
992
|
() =>
|
|
974
993
|
createWorkspaceSsoEmbedSession({
|
|
975
|
-
app: "
|
|
976
|
-
path: "/",
|
|
994
|
+
app: "analytics",
|
|
995
|
+
path: "/overview",
|
|
977
996
|
}),
|
|
978
997
|
);
|
|
979
998
|
|
|
980
|
-
expect(result).
|
|
981
|
-
|
|
982
|
-
startUrl:
|
|
983
|
-
"https://agent-workspace.builder.io/atlas/_agent-native/embed/start?ticket=remote",
|
|
984
|
-
});
|
|
985
|
-
expect(mocks.managerConstructor).toHaveBeenCalledWith({
|
|
986
|
-
servers: {
|
|
987
|
-
target: expect.objectContaining({
|
|
988
|
-
url: "https://agent-workspace.builder.io/atlas/mcp",
|
|
989
|
-
}),
|
|
990
|
-
},
|
|
991
|
-
});
|
|
999
|
+
expect(result.app).toBe("analytics");
|
|
1000
|
+
expect(mocks.managerConstructor).toHaveBeenCalled();
|
|
992
1001
|
});
|
|
993
1002
|
|
|
994
1003
|
it("allows an exact custom registry entry and rejects an unregistered external app", async () => {
|
|
@@ -1263,6 +1272,74 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
1263
1272
|
);
|
|
1264
1273
|
});
|
|
1265
1274
|
|
|
1275
|
+
it("falls back to the shared A2A secret when the target rejects org signing", async () => {
|
|
1276
|
+
vi.stubEnv("A2A_SECRET", "shared-secret");
|
|
1277
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
1278
|
+
mocks.getOrgA2ASecret.mockResolvedValue("org-specific-secret");
|
|
1279
|
+
mocks.signA2AToken
|
|
1280
|
+
.mockResolvedValueOnce("org-signed-token")
|
|
1281
|
+
.mockResolvedValueOnce("global-signed-token");
|
|
1282
|
+
mocks.managerCallTool
|
|
1283
|
+
.mockRejectedValueOnce(
|
|
1284
|
+
new Error(
|
|
1285
|
+
'MCP server "target" is not connected: HTTP 401 Unauthorized',
|
|
1286
|
+
),
|
|
1287
|
+
)
|
|
1288
|
+
.mockResolvedValueOnce({
|
|
1289
|
+
structuredContent: {
|
|
1290
|
+
startUrl:
|
|
1291
|
+
"http://localhost:8086/_agent-native/embed/start?ticket=remote",
|
|
1292
|
+
},
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
const result = await runWithRequestContext(
|
|
1296
|
+
{
|
|
1297
|
+
userEmail: "owner@example.test",
|
|
1298
|
+
orgId: "org-1",
|
|
1299
|
+
requestOrigin: "http://localhost:8092",
|
|
1300
|
+
},
|
|
1301
|
+
() =>
|
|
1302
|
+
createGrantedDispatchMcpEmbedSession({
|
|
1303
|
+
app: "analytics",
|
|
1304
|
+
path: "/dashboards",
|
|
1305
|
+
}),
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
expect(result).toMatchObject({ app: "analytics" });
|
|
1309
|
+
expect(mocks.managerConstructor).toHaveBeenCalledTimes(2);
|
|
1310
|
+
expect(mocks.managerConstructor).toHaveBeenNthCalledWith(
|
|
1311
|
+
1,
|
|
1312
|
+
expect.objectContaining({
|
|
1313
|
+
servers: {
|
|
1314
|
+
target: expect.objectContaining({
|
|
1315
|
+
headers: { Authorization: "Bearer org-signed-token" },
|
|
1316
|
+
}),
|
|
1317
|
+
},
|
|
1318
|
+
}),
|
|
1319
|
+
);
|
|
1320
|
+
expect(mocks.managerConstructor).toHaveBeenNthCalledWith(
|
|
1321
|
+
2,
|
|
1322
|
+
expect.objectContaining({
|
|
1323
|
+
servers: {
|
|
1324
|
+
target: expect.objectContaining({
|
|
1325
|
+
headers: { Authorization: "Bearer global-signed-token" },
|
|
1326
|
+
}),
|
|
1327
|
+
},
|
|
1328
|
+
}),
|
|
1329
|
+
);
|
|
1330
|
+
expect(mocks.signA2AToken).toHaveBeenNthCalledWith(
|
|
1331
|
+
2,
|
|
1332
|
+
"owner@example.test",
|
|
1333
|
+
"builder.io",
|
|
1334
|
+
undefined,
|
|
1335
|
+
{
|
|
1336
|
+
expiresIn: "5m",
|
|
1337
|
+
audience: "http://localhost:8086",
|
|
1338
|
+
preferGlobalSecret: true,
|
|
1339
|
+
},
|
|
1340
|
+
);
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1266
1343
|
it("falls back to the shared A2A secret when no org secret is available", async () => {
|
|
1267
1344
|
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
1268
1345
|
mocks.getOrgA2ASecret.mockResolvedValue(null);
|
|
@@ -626,17 +626,6 @@ function workspaceSsoOriginForApp(appId: string): string | null {
|
|
|
626
626
|
return custom?.origin ?? null;
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
-
function workspaceSsoGatewayOrigin(): string | null {
|
|
630
|
-
const configured = process.env.WORKSPACE_GATEWAY_URL?.trim();
|
|
631
|
-
if (!configured) return null;
|
|
632
|
-
try {
|
|
633
|
-
return new URL(configured).origin;
|
|
634
|
-
} catch {
|
|
635
|
-
// coercion-ok: malformed deployment configuration is not a trusted origin.
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
629
|
function isWorkspaceSsoCanonicalApp(app: DispatchMcpAccessibleApp): boolean {
|
|
641
630
|
const origin = safeAppOrigin(app);
|
|
642
631
|
if (!origin) return false;
|
|
@@ -653,17 +642,8 @@ function isWorkspaceSsoCanonicalApp(app: DispatchMcpAccessibleApp): boolean {
|
|
|
653
642
|
: false;
|
|
654
643
|
}
|
|
655
644
|
|
|
656
|
-
function sameWorkspaceApp(
|
|
657
|
-
candidate: DispatchMcpAccessibleApp,
|
|
658
|
-
mounted: WorkspaceAppSummary,
|
|
659
|
-
): boolean {
|
|
660
|
-
if (candidate.id !== mounted.id || !mounted.url) return false;
|
|
661
|
-
return candidate.url.replace(/\/+$/, "") === mounted.url.replace(/\/+$/, "");
|
|
662
|
-
}
|
|
663
|
-
|
|
664
645
|
async function isEligibleWorkspaceSsoApp(
|
|
665
646
|
candidate: DispatchMcpAccessibleApp,
|
|
666
|
-
mountedApps: WorkspaceAppSummary[],
|
|
667
647
|
): Promise<boolean> {
|
|
668
648
|
const origin = safeAppOrigin(candidate);
|
|
669
649
|
if (!origin) return false;
|
|
@@ -671,18 +651,7 @@ async function isEligibleWorkspaceSsoApp(
|
|
|
671
651
|
|
|
672
652
|
const customOrigin = workspaceSsoOriginForApp(candidate.id);
|
|
673
653
|
if (customOrigin === origin) return true;
|
|
674
|
-
|
|
675
|
-
const dispatchOrigin = safeAppOrigin({
|
|
676
|
-
id: DISPATCH_APP_ID,
|
|
677
|
-
name: DISPATCH_NAME,
|
|
678
|
-
description: DISPATCH_DESCRIPTION,
|
|
679
|
-
url: dispatchSelfBaseUrl(),
|
|
680
|
-
color: DISPATCH_COLOR,
|
|
681
|
-
granted: true,
|
|
682
|
-
});
|
|
683
|
-
const trustedMountedOrigin = workspaceSsoGatewayOrigin() ?? dispatchOrigin;
|
|
684
|
-
if (origin !== trustedMountedOrigin) return false;
|
|
685
|
-
return mountedApps.some((mounted) => sameWorkspaceApp(candidate, mounted));
|
|
654
|
+
return false;
|
|
686
655
|
}
|
|
687
656
|
|
|
688
657
|
async function listWorkspaceSsoApps(): Promise<DispatchMcpAccessibleApp[]> {
|
|
@@ -702,8 +671,8 @@ async function listWorkspaceSsoApps(): Promise<DispatchMcpAccessibleApp[]> {
|
|
|
702
671
|
});
|
|
703
672
|
}
|
|
704
673
|
// The hosted Dispatch database is separate from the shared Workspace
|
|
705
|
-
// database. Overlay the live mounted registry so custom apps
|
|
706
|
-
// without copying a stale app list into Dispatch configuration.
|
|
674
|
+
// database. Overlay the live mounted registry so custom apps remain
|
|
675
|
+
// discoverable without copying a stale app list into Dispatch configuration.
|
|
707
676
|
for (const app of mountedApps) {
|
|
708
677
|
if (app.isDispatch || !app.url) continue;
|
|
709
678
|
candidatesById.set(app.id, {
|
|
@@ -718,7 +687,7 @@ async function listWorkspaceSsoApps(): Promise<DispatchMcpAccessibleApp[]> {
|
|
|
718
687
|
const candidates = [...candidatesById.values()];
|
|
719
688
|
const eligible = [] as DispatchMcpAccessibleApp[];
|
|
720
689
|
for (const candidate of candidates) {
|
|
721
|
-
if (await isEligibleWorkspaceSsoApp(candidate
|
|
690
|
+
if (await isEligibleWorkspaceSsoApp(candidate)) {
|
|
722
691
|
eligible.push(candidate);
|
|
723
692
|
}
|
|
724
693
|
}
|
|
@@ -975,6 +944,46 @@ function isRetryableTargetMcpError(error: unknown): boolean {
|
|
|
975
944
|
);
|
|
976
945
|
}
|
|
977
946
|
|
|
947
|
+
function isTargetMcpAuthError(error: unknown): boolean {
|
|
948
|
+
const message =
|
|
949
|
+
error instanceof Error
|
|
950
|
+
? error.message
|
|
951
|
+
: typeof error === "string"
|
|
952
|
+
? error
|
|
953
|
+
: String(error ?? "");
|
|
954
|
+
return /\b401\b|\b403\b|unauthorized|forbidden|invalid(?: or expired)? (?:a2a )?token|authentication required/i.test(
|
|
955
|
+
message,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function targetMcpErrorStatus(error: unknown): number | undefined {
|
|
960
|
+
const message =
|
|
961
|
+
error instanceof Error
|
|
962
|
+
? error.message
|
|
963
|
+
: typeof error === "string"
|
|
964
|
+
? error
|
|
965
|
+
: String(error ?? "");
|
|
966
|
+
const status = message.match(/\b([45]\d{2})\b/)?.[1];
|
|
967
|
+
return status ? Number(status) : undefined;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
type TargetMcpTokenAttempt = {
|
|
971
|
+
token: string;
|
|
972
|
+
strategy: "org" | "global";
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
function targetMcpRequestDetails(input: {
|
|
976
|
+
app: DispatchMcpAccessibleApp;
|
|
977
|
+
url: string;
|
|
978
|
+
}): { app: string; targetOrigin: string; targetPath: string } {
|
|
979
|
+
const targetUrl = new URL(input.url);
|
|
980
|
+
return {
|
|
981
|
+
app: input.app.id,
|
|
982
|
+
targetOrigin: targetUrl.origin,
|
|
983
|
+
targetPath: targetUrl.pathname,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
978
987
|
function targetMcpRetryDelay(attempt: number): number {
|
|
979
988
|
const base =
|
|
980
989
|
TARGET_EMBED_SESSION_RETRY_BASE_MS * Math.pow(2, Math.max(0, attempt - 1));
|
|
@@ -1025,6 +1034,58 @@ async function callTargetCreateEmbedSession(input: {
|
|
|
1025
1034
|
}
|
|
1026
1035
|
}
|
|
1027
1036
|
|
|
1037
|
+
async function createTargetMcpTokenAttempts(input: {
|
|
1038
|
+
ownerEmail: string;
|
|
1039
|
+
orgDomain?: string;
|
|
1040
|
+
orgSecret?: string;
|
|
1041
|
+
target: DispatchMcpAccessibleApp;
|
|
1042
|
+
}): Promise<TargetMcpTokenAttempt[]> {
|
|
1043
|
+
const attempts: TargetMcpTokenAttempt[] = [];
|
|
1044
|
+
const addAttempt = async (tokenInput: {
|
|
1045
|
+
strategy: TargetMcpTokenAttempt["strategy"];
|
|
1046
|
+
secret?: string;
|
|
1047
|
+
preferGlobalSecret: boolean;
|
|
1048
|
+
}) => {
|
|
1049
|
+
const token = await signA2AToken(
|
|
1050
|
+
input.ownerEmail,
|
|
1051
|
+
input.orgDomain,
|
|
1052
|
+
tokenInput.secret,
|
|
1053
|
+
{
|
|
1054
|
+
expiresIn: "5m",
|
|
1055
|
+
audience: canonicalA2AAudience(appBaseUrl(input.target)),
|
|
1056
|
+
preferGlobalSecret: tokenInput.preferGlobalSecret,
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
1059
|
+
if (!attempts.some((attempt) => attempt.token === token)) {
|
|
1060
|
+
attempts.push({ token, strategy: tokenInput.strategy });
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
if (input.orgDomain && input.orgSecret) {
|
|
1065
|
+
await addAttempt({
|
|
1066
|
+
strategy: "org",
|
|
1067
|
+
secret: input.orgSecret,
|
|
1068
|
+
preferGlobalSecret: false,
|
|
1069
|
+
});
|
|
1070
|
+
// A target app may not have the org secret synced yet. The shared secret
|
|
1071
|
+
// is a bounded compatibility fallback, used only after the target rejects
|
|
1072
|
+
// the org-signed request and never after a non-authentication failure.
|
|
1073
|
+
if (process.env.A2A_SECRET?.trim()) {
|
|
1074
|
+
await addAttempt({
|
|
1075
|
+
strategy: "global",
|
|
1076
|
+
preferGlobalSecret: true,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
await addAttempt({
|
|
1081
|
+
strategy: "global",
|
|
1082
|
+
preferGlobalSecret: true,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return attempts;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1028
1089
|
async function resolveEmbedTarget(
|
|
1029
1090
|
input: {
|
|
1030
1091
|
app?: string;
|
|
@@ -1192,43 +1253,93 @@ async function createEmbedSessionForResolvedApp(input: {
|
|
|
1192
1253
|
typeof orgSecret === "string" && orgSecret.trim().length > 0;
|
|
1193
1254
|
const usableOrgDomain =
|
|
1194
1255
|
typeof orgDomain === "string" && orgDomain.trim().length > 0;
|
|
1195
|
-
const useOrgSigning = usableOrgDomain && usableOrgSecret;
|
|
1196
1256
|
const signedOrgDomain = usableOrgDomain ? orgDomain.trim() : undefined;
|
|
1197
|
-
const
|
|
1257
|
+
const tokenAttempts = await createTargetMcpTokenAttempts({
|
|
1198
1258
|
ownerEmail,
|
|
1199
|
-
signedOrgDomain,
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
// Prefer the synced org A2A secret when present because first-party
|
|
1205
|
-
// production apps do not have to share the same deployment env secret.
|
|
1206
|
-
// Fall back to the global A2A_SECRET for orgs that have not synced yet.
|
|
1207
|
-
preferGlobalSecret: !useOrgSigning,
|
|
1208
|
-
},
|
|
1209
|
-
);
|
|
1210
|
-
|
|
1211
|
-
const result = await callTargetCreateEmbedSession({
|
|
1259
|
+
orgDomain: signedOrgDomain,
|
|
1260
|
+
orgSecret: usableOrgSecret ? orgSecret.trim() : undefined,
|
|
1261
|
+
target: target.app,
|
|
1262
|
+
});
|
|
1263
|
+
const targetDetails = targetMcpRequestDetails({
|
|
1212
1264
|
app: target.app,
|
|
1213
|
-
token,
|
|
1214
1265
|
url: target.url,
|
|
1215
|
-
chrome,
|
|
1216
1266
|
});
|
|
1217
|
-
|
|
1267
|
+
let parsed: {
|
|
1218
1268
|
startUrl?: string;
|
|
1219
1269
|
targetPath?: string;
|
|
1220
1270
|
expiresAt?: number;
|
|
1221
|
-
};
|
|
1271
|
+
} | null = null;
|
|
1272
|
+
let lastError: unknown;
|
|
1273
|
+
for (
|
|
1274
|
+
let attemptIndex = 0;
|
|
1275
|
+
attemptIndex < tokenAttempts.length;
|
|
1276
|
+
attemptIndex++
|
|
1277
|
+
) {
|
|
1278
|
+
const tokenAttempt = tokenAttempts[attemptIndex];
|
|
1279
|
+
console.info("[dispatch] workspace embed target request", {
|
|
1280
|
+
...targetDetails,
|
|
1281
|
+
authStrategy: tokenAttempt.strategy,
|
|
1282
|
+
attempt: attemptIndex + 1,
|
|
1283
|
+
attempts: tokenAttempts.length,
|
|
1284
|
+
});
|
|
1285
|
+
try {
|
|
1286
|
+
const result = await callTargetCreateEmbedSession({
|
|
1287
|
+
app: target.app,
|
|
1288
|
+
token: tokenAttempt.token,
|
|
1289
|
+
url: target.url,
|
|
1290
|
+
chrome,
|
|
1291
|
+
});
|
|
1292
|
+
parsed = parseMcpToolTextResult(result) as {
|
|
1293
|
+
startUrl?: string;
|
|
1294
|
+
targetPath?: string;
|
|
1295
|
+
expiresAt?: number;
|
|
1296
|
+
};
|
|
1297
|
+
break;
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
lastError = error;
|
|
1300
|
+
console.warn("[dispatch] workspace embed target response", {
|
|
1301
|
+
...targetDetails,
|
|
1302
|
+
authStrategy: tokenAttempt.strategy,
|
|
1303
|
+
attempt: attemptIndex + 1,
|
|
1304
|
+
status: targetMcpErrorStatus(error),
|
|
1305
|
+
category: isTargetMcpAuthError(error)
|
|
1306
|
+
? "authentication"
|
|
1307
|
+
: isRetryableTargetMcpError(error)
|
|
1308
|
+
? "transient"
|
|
1309
|
+
: "permanent",
|
|
1310
|
+
});
|
|
1311
|
+
if (
|
|
1312
|
+
!isTargetMcpAuthError(error) ||
|
|
1313
|
+
attemptIndex >= tokenAttempts.length - 1
|
|
1314
|
+
) {
|
|
1315
|
+
throw error;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (!parsed) {
|
|
1320
|
+
throw lastError instanceof Error
|
|
1321
|
+
? lastError
|
|
1322
|
+
: new Error("Target app did not return an embed session.");
|
|
1323
|
+
}
|
|
1222
1324
|
if (!parsed.startUrl) {
|
|
1223
1325
|
throw new Error("Target app did not return an embed start URL.");
|
|
1224
1326
|
}
|
|
1327
|
+
const startUrl = resolveGrantedAppEmbedStartUrl(target.app, parsed.startUrl);
|
|
1328
|
+
const startUrlDetails = new URL(startUrl);
|
|
1329
|
+
console.info("[dispatch] workspace embed target minted session", {
|
|
1330
|
+
...targetDetails,
|
|
1331
|
+
startPath: startUrlDetails.pathname,
|
|
1332
|
+
returnedTicket: startUrlDetails.searchParams.has("ticket"),
|
|
1333
|
+
returnedTargetPath: typeof parsed.targetPath === "string",
|
|
1334
|
+
returnedExpiry: typeof parsed.expiresAt === "number",
|
|
1335
|
+
});
|
|
1225
1336
|
const output: {
|
|
1226
1337
|
startUrl: string;
|
|
1227
1338
|
targetPath?: string;
|
|
1228
1339
|
expiresAt?: number;
|
|
1229
1340
|
app: string;
|
|
1230
1341
|
} = {
|
|
1231
|
-
startUrl
|
|
1342
|
+
startUrl,
|
|
1232
1343
|
app: target.app.id,
|
|
1233
1344
|
};
|
|
1234
1345
|
if (parsed.targetPath) output.targetPath = parsed.targetPath;
|
|
@@ -1248,13 +1359,31 @@ export async function createWorkspaceSsoEmbedSession(input: {
|
|
|
1248
1359
|
app: string;
|
|
1249
1360
|
}> {
|
|
1250
1361
|
const ownerEmail = getRequestUserEmail();
|
|
1251
|
-
if (!ownerEmail)
|
|
1362
|
+
if (!ownerEmail) {
|
|
1363
|
+
console.warn("[dispatch] workspace embed mint rejected", {
|
|
1364
|
+
phase: "dispatch-auth",
|
|
1365
|
+
requestedApp: input.app ?? null,
|
|
1366
|
+
hasPath: typeof input.path === "string",
|
|
1367
|
+
hasUrl: typeof input.url === "string",
|
|
1368
|
+
ownerResolved: false,
|
|
1369
|
+
orgResolved: Boolean(getRequestOrgId()),
|
|
1370
|
+
});
|
|
1371
|
+
throw new Error("no authenticated user");
|
|
1372
|
+
}
|
|
1252
1373
|
const enabled = await isFeatureFlagEnabled(DISPATCH_WORKSPACE_SSO_FLAG, {
|
|
1253
1374
|
userEmail: ownerEmail,
|
|
1254
1375
|
userKey: ownerEmail,
|
|
1255
1376
|
orgId: getRequestOrgId(),
|
|
1256
1377
|
});
|
|
1257
1378
|
if (!enabled) {
|
|
1379
|
+
console.warn("[dispatch] workspace embed mint rejected", {
|
|
1380
|
+
phase: "feature-flag",
|
|
1381
|
+
requestedApp: input.app ?? null,
|
|
1382
|
+
hasPath: typeof input.path === "string",
|
|
1383
|
+
hasUrl: typeof input.url === "string",
|
|
1384
|
+
ownerResolved: true,
|
|
1385
|
+
orgResolved: Boolean(getRequestOrgId()),
|
|
1386
|
+
});
|
|
1258
1387
|
throw new Error("Dispatch workspace sign-in is not enabled.");
|
|
1259
1388
|
}
|
|
1260
1389
|
|