@agent-native/dispatch 0.30.2 → 0.30.5
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/ask_app.d.ts +1 -0
- package/dist/actions/ask_app_status.d.ts +1 -0
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +45 -18
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +149 -7
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/lib/dispatch-store.d.ts +1 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +4 -0
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +8 -2
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +39 -6
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/package.json +4 -4
- package/src/routes/pages/chat.spec.tsx +49 -0
- package/src/routes/pages/chat.tsx +51 -0
- package/src/server/lib/dispatch-integrations.spec.ts +578 -7
- package/src/server/lib/dispatch-integrations.ts +186 -9
- package/src/server/lib/dispatch-store.ts +7 -0
- package/src/server/lib/mcp-gateway.spec.ts +328 -17
- package/src/server/lib/mcp-gateway.ts +59 -4
|
@@ -2,6 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
const mocks = vi.hoisted(() => ({
|
|
4
4
|
consumeLinkToken: vi.fn(),
|
|
5
|
+
evaluateIntegrationScopePolicy: vi.fn(),
|
|
6
|
+
getActiveIntegrationInstallationByKey: vi.fn(),
|
|
7
|
+
getIntegrationScope: vi.fn(),
|
|
8
|
+
isOrgMember: vi.fn(),
|
|
5
9
|
resolveLinkedOwner: vi.fn(),
|
|
6
10
|
resolveOrgIdForEmail: vi.fn(),
|
|
7
11
|
resolveSecret: vi.fn(),
|
|
@@ -19,11 +23,16 @@ vi.mock("@agent-native/core/integrations", async () => {
|
|
|
19
23
|
>("@agent-native/core/integrations");
|
|
20
24
|
return {
|
|
21
25
|
...actual,
|
|
26
|
+
getActiveIntegrationInstallationByKey:
|
|
27
|
+
mocks.getActiveIntegrationInstallationByKey,
|
|
28
|
+
evaluateIntegrationScopePolicy: mocks.evaluateIntegrationScopePolicy,
|
|
29
|
+
getIntegrationScope: mocks.getIntegrationScope,
|
|
22
30
|
resolveSlackBotTokenForIncoming: mocks.resolveSlackBotTokenForIncoming,
|
|
23
31
|
};
|
|
24
32
|
});
|
|
25
33
|
|
|
26
34
|
vi.mock("@agent-native/core/org", () => ({
|
|
35
|
+
isOrgMember: mocks.isOrgMember,
|
|
27
36
|
resolveOrgIdForEmail: mocks.resolveOrgIdForEmail,
|
|
28
37
|
}));
|
|
29
38
|
|
|
@@ -114,6 +123,10 @@ const noopAdapter: PlatformAdapter = {
|
|
|
114
123
|
};
|
|
115
124
|
|
|
116
125
|
beforeEach(() => {
|
|
126
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValue(null);
|
|
127
|
+
mocks.getIntegrationScope.mockResolvedValue(null);
|
|
128
|
+
mocks.evaluateIntegrationScopePolicy.mockReturnValue({ allowed: true });
|
|
129
|
+
mocks.isOrgMember.mockResolvedValue(false);
|
|
117
130
|
mocks.resolveLinkedOwner.mockResolvedValue(null);
|
|
118
131
|
mocks.consumeLinkToken.mockResolvedValue("owner@example.test");
|
|
119
132
|
mocks.resolveOrgIdForEmail.mockResolvedValue(null);
|
|
@@ -127,6 +140,16 @@ beforeEach(() => {
|
|
|
127
140
|
);
|
|
128
141
|
});
|
|
129
142
|
|
|
143
|
+
function managedSlackInstallation(overrides: Record<string, unknown> = {}) {
|
|
144
|
+
return {
|
|
145
|
+
id: "installation-managed",
|
|
146
|
+
installationKey: "T-MANAGED",
|
|
147
|
+
ownerEmail: "installer@example.test",
|
|
148
|
+
orgId: "org-managed",
|
|
149
|
+
...overrides,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
afterEach(() => {
|
|
131
154
|
vi.unstubAllEnvs();
|
|
132
155
|
vi.unstubAllGlobals();
|
|
@@ -374,7 +397,7 @@ describe("beforeDispatchProcess", () => {
|
|
|
374
397
|
});
|
|
375
398
|
});
|
|
376
399
|
|
|
377
|
-
it("
|
|
400
|
+
it("fails closed instead of silently dropping an unmatched Slack DM", async () => {
|
|
378
401
|
vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
|
|
379
402
|
const incoming = slackIncoming({
|
|
380
403
|
triggerKind: "dm",
|
|
@@ -390,15 +413,15 @@ describe("beforeDispatchProcess", () => {
|
|
|
390
413
|
const result = await beforeDispatchProcess(incoming, noopAdapter);
|
|
391
414
|
|
|
392
415
|
expect(execution.ownerEmail).toMatch(/@integration\.local$/);
|
|
393
|
-
expect(incoming.platformContext.
|
|
416
|
+
expect(incoming.platformContext.identityVerificationFailed).toBe(true);
|
|
394
417
|
expect(result).toEqual({
|
|
395
418
|
handled: true,
|
|
396
419
|
responseText:
|
|
397
|
-
"
|
|
420
|
+
"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
|
|
398
421
|
});
|
|
399
422
|
});
|
|
400
423
|
|
|
401
|
-
it("
|
|
424
|
+
it("does not let an unmatched Slack DM consume a link token", async () => {
|
|
402
425
|
const incoming = slackIncoming({
|
|
403
426
|
text: "/link token-123",
|
|
404
427
|
triggerKind: "dm",
|
|
@@ -416,13 +439,561 @@ describe("beforeDispatchProcess", () => {
|
|
|
416
439
|
expect(result).toEqual({
|
|
417
440
|
handled: true,
|
|
418
441
|
responseText:
|
|
419
|
-
"
|
|
442
|
+
"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
|
|
420
443
|
});
|
|
444
|
+
expect(mocks.consumeLinkToken).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("scopes a managed Slack link claim to the installation organization", async () => {
|
|
448
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
449
|
+
managedSlackInstallation(),
|
|
450
|
+
);
|
|
451
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
452
|
+
"managed-token",
|
|
453
|
+
);
|
|
454
|
+
mocks.consumeLinkToken.mockRejectedValueOnce(
|
|
455
|
+
new Error(
|
|
456
|
+
"This link token belongs to a different organization. Create a token from this workspace and try again.",
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
460
|
+
new Response(
|
|
461
|
+
JSON.stringify({
|
|
462
|
+
ok: true,
|
|
463
|
+
user: { profile: { email: "outside@example.test" } },
|
|
464
|
+
}),
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
const incoming = slackIncoming({
|
|
468
|
+
text: "/link token-other-org",
|
|
469
|
+
senderId: "U-MANAGED-LINK",
|
|
470
|
+
triggerKind: "dm",
|
|
471
|
+
conversationType: "dm",
|
|
472
|
+
platformContext: {
|
|
473
|
+
teamId: "T-MANAGED",
|
|
474
|
+
channelId: "D-MANAGED",
|
|
475
|
+
channelType: "im",
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
480
|
+
{
|
|
481
|
+
handled: true,
|
|
482
|
+
responseText:
|
|
483
|
+
"This link token belongs to a different organization. Create a token from this workspace and try again.",
|
|
484
|
+
},
|
|
485
|
+
);
|
|
421
486
|
expect(mocks.consumeLinkToken).toHaveBeenCalledWith({
|
|
422
487
|
platform: "slack",
|
|
423
|
-
token: "token-
|
|
424
|
-
externalUserId: "
|
|
488
|
+
token: "token-other-org",
|
|
489
|
+
externalUserId: "T-MANAGED:U-MANAGED-LINK",
|
|
425
490
|
externalUserName: "U123",
|
|
491
|
+
expectedOrgId: "org-managed",
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("does not consume a managed Slack link token when identity verification fails", async () => {
|
|
496
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
497
|
+
managedSlackInstallation(),
|
|
498
|
+
);
|
|
499
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
500
|
+
"managed-token",
|
|
501
|
+
);
|
|
502
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
503
|
+
new Response(JSON.stringify({ ok: false })),
|
|
504
|
+
);
|
|
505
|
+
const incoming = slackIncoming({
|
|
506
|
+
text: "/link token-unverified",
|
|
507
|
+
senderId: "U-MANAGED-UNVERIFIED",
|
|
508
|
+
triggerKind: "dm",
|
|
509
|
+
conversationType: "dm",
|
|
510
|
+
platformContext: {
|
|
511
|
+
teamId: "T-MANAGED",
|
|
512
|
+
channelId: "D-MANAGED",
|
|
513
|
+
channelType: "im",
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
518
|
+
{
|
|
519
|
+
handled: true,
|
|
520
|
+
responseText:
|
|
521
|
+
"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
expect(mocks.consumeLinkToken).not.toHaveBeenCalled();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe("managed Slack execution identity", () => {
|
|
529
|
+
it("fails closed when no managed installation matches a Slack DM", async () => {
|
|
530
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
531
|
+
const incoming = slackIncoming({
|
|
532
|
+
senderId: "U-UNMATCHED",
|
|
533
|
+
triggerKind: "dm",
|
|
534
|
+
conversationType: "dm",
|
|
535
|
+
platformContext: { teamId: "T-UNMATCHED", channelId: "D-UNMATCHED" },
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const execution = await resolveDispatchExecutionContext(incoming);
|
|
539
|
+
|
|
540
|
+
expect(execution.ownerEmail).toMatch(/@integration\.local$/);
|
|
541
|
+
expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
|
|
542
|
+
expect(execution.orgId).toBeNull();
|
|
543
|
+
expect(incoming.platformContext.identityVerificationFailed).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("uses the enterprise-scoped installation for an Enterprise Grid DM", async () => {
|
|
547
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
548
|
+
managedSlackInstallation(),
|
|
549
|
+
);
|
|
550
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
551
|
+
"managed-token",
|
|
552
|
+
);
|
|
553
|
+
mocks.isOrgMember.mockResolvedValueOnce(true);
|
|
554
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
555
|
+
new Response(
|
|
556
|
+
JSON.stringify({
|
|
557
|
+
ok: true,
|
|
558
|
+
user: { profile: { email: "alice@example.test" } },
|
|
559
|
+
}),
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
await resolveDispatchExecutionContext(
|
|
564
|
+
slackIncoming({
|
|
565
|
+
senderId: "U-ENTERPRISE-ALICE",
|
|
566
|
+
triggerKind: "dm",
|
|
567
|
+
conversationType: "dm",
|
|
568
|
+
platformContext: {
|
|
569
|
+
teamId: "T-ENTERPRISE-WORKSPACE",
|
|
570
|
+
enterpriseId: "E-MANAGED",
|
|
571
|
+
isEnterpriseInstall: true,
|
|
572
|
+
apiAppId: "A-MANAGED",
|
|
573
|
+
channelId: "D-MANAGED",
|
|
574
|
+
channelType: "im",
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
expect(mocks.getActiveIntegrationInstallationByKey).toHaveBeenCalledWith(
|
|
580
|
+
"slack",
|
|
581
|
+
"enterprise:E-MANAGED:app:A-MANAGED",
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("fails closed with retry guidance when managed installation lookup is unavailable", async () => {
|
|
586
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
587
|
+
mocks.getActiveIntegrationInstallationByKey.mockRejectedValueOnce(
|
|
588
|
+
new Error("installation store unavailable"),
|
|
589
|
+
);
|
|
590
|
+
const incoming = slackIncoming({
|
|
591
|
+
senderId: "U-MANAGED-ALICE",
|
|
592
|
+
triggerKind: "dm",
|
|
593
|
+
conversationType: "dm",
|
|
594
|
+
platformContext: {
|
|
595
|
+
teamId: "T-MANAGED",
|
|
596
|
+
channelId: "D-MANAGED",
|
|
597
|
+
channelType: "im",
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const execution = await resolveDispatchExecutionContext(incoming);
|
|
602
|
+
|
|
603
|
+
expect(execution.ownerEmail).toMatch(/@integration\.local$/);
|
|
604
|
+
expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
|
|
605
|
+
expect(execution.orgId).toBeNull();
|
|
606
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
607
|
+
{
|
|
608
|
+
handled: true,
|
|
609
|
+
responseText:
|
|
610
|
+
"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
|
|
611
|
+
},
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("fails closed with retry guidance when linked identity lookup is unavailable", async () => {
|
|
616
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
617
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
618
|
+
managedSlackInstallation(),
|
|
619
|
+
);
|
|
620
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
621
|
+
"managed-token",
|
|
622
|
+
);
|
|
623
|
+
mocks.resolveLinkedOwner.mockRejectedValueOnce(
|
|
624
|
+
new Error("identity link store unavailable"),
|
|
625
|
+
);
|
|
626
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
627
|
+
new Response(
|
|
628
|
+
JSON.stringify({
|
|
629
|
+
ok: true,
|
|
630
|
+
user: { profile: { email: "alice@example.test" } },
|
|
631
|
+
}),
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
const incoming = slackIncoming({
|
|
635
|
+
senderId: "U-MANAGED-ALICE",
|
|
636
|
+
triggerKind: "dm",
|
|
637
|
+
conversationType: "dm",
|
|
638
|
+
platformContext: {
|
|
639
|
+
teamId: "T-MANAGED",
|
|
640
|
+
channelId: "D-MANAGED",
|
|
641
|
+
channelType: "im",
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const execution = await resolveDispatchExecutionContext(incoming);
|
|
646
|
+
|
|
647
|
+
expect(execution.ownerEmail).toMatch(/@integration\.local$/);
|
|
648
|
+
expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
|
|
649
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
650
|
+
{
|
|
651
|
+
handled: true,
|
|
652
|
+
responseText:
|
|
653
|
+
"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
|
|
654
|
+
},
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("does not let a stale identity link override the verified Slack email", async () => {
|
|
659
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
660
|
+
managedSlackInstallation(),
|
|
661
|
+
);
|
|
662
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
663
|
+
"managed-token",
|
|
664
|
+
);
|
|
665
|
+
mocks.resolveLinkedOwner.mockResolvedValueOnce("stale@example.test");
|
|
666
|
+
mocks.isOrgMember.mockImplementation(
|
|
667
|
+
async (_orgId, email) => email === "current@example.test",
|
|
668
|
+
);
|
|
669
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
670
|
+
new Response(
|
|
671
|
+
JSON.stringify({
|
|
672
|
+
ok: true,
|
|
673
|
+
user: { profile: { email: "current@example.test" } },
|
|
674
|
+
}),
|
|
675
|
+
),
|
|
676
|
+
);
|
|
677
|
+
const incoming = slackIncoming({
|
|
678
|
+
senderId: "U-MANAGED-MEMBER",
|
|
679
|
+
triggerKind: "dm",
|
|
680
|
+
conversationType: "dm",
|
|
681
|
+
platformContext: {
|
|
682
|
+
teamId: "T-MANAGED",
|
|
683
|
+
channelId: "D-MANAGED",
|
|
684
|
+
channelType: "im",
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
await expect(
|
|
689
|
+
resolveDispatchExecutionContext(incoming),
|
|
690
|
+
).resolves.toMatchObject({
|
|
691
|
+
ownerEmail: "current@example.test",
|
|
692
|
+
orgId: "org-managed",
|
|
693
|
+
principalType: "user",
|
|
694
|
+
});
|
|
695
|
+
expect(mocks.isOrgMember).not.toHaveBeenCalledWith(
|
|
696
|
+
"org-managed",
|
|
697
|
+
"stale@example.test",
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("does not borrow a user principal when managed channel installation lookup is unavailable", async () => {
|
|
702
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
703
|
+
mocks.getActiveIntegrationInstallationByKey.mockRejectedValueOnce(
|
|
704
|
+
new Error("installation store unavailable"),
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
await expect(
|
|
708
|
+
resolveDispatchExecutionContext(
|
|
709
|
+
slackIncoming({
|
|
710
|
+
senderId: "U-MANAGED-ALICE",
|
|
711
|
+
triggerKind: "mention",
|
|
712
|
+
conversationType: "channel",
|
|
713
|
+
platformContext: {
|
|
714
|
+
teamId: "T-MANAGED",
|
|
715
|
+
channelId: "C-MANAGED",
|
|
716
|
+
channelType: "channel",
|
|
717
|
+
},
|
|
718
|
+
}),
|
|
719
|
+
),
|
|
720
|
+
).rejects.toThrow(
|
|
721
|
+
"Managed Slack installation identity is temporarily unavailable",
|
|
722
|
+
);
|
|
723
|
+
expect(mocks.resolveLinkedOwner).not.toHaveBeenCalled();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("uses the verified member in the installation org and ignores the deployment default", async () => {
|
|
727
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
728
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
729
|
+
managedSlackInstallation(),
|
|
730
|
+
);
|
|
731
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
732
|
+
"managed-token",
|
|
733
|
+
);
|
|
734
|
+
mocks.isOrgMember.mockResolvedValueOnce(true);
|
|
735
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
736
|
+
new Response(
|
|
737
|
+
JSON.stringify({
|
|
738
|
+
ok: true,
|
|
739
|
+
user: {
|
|
740
|
+
profile: {
|
|
741
|
+
email: "ALICE@EXAMPLE.TEST",
|
|
742
|
+
display_name: "Alice",
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
}),
|
|
746
|
+
),
|
|
747
|
+
);
|
|
748
|
+
const incoming = slackIncoming({
|
|
749
|
+
senderId: "U-MANAGED-ALICE",
|
|
750
|
+
triggerKind: "dm",
|
|
751
|
+
conversationType: "dm",
|
|
752
|
+
platformContext: {
|
|
753
|
+
teamId: "T-MANAGED",
|
|
754
|
+
channelId: "D-MANAGED",
|
|
755
|
+
channelType: "im",
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
await expect(resolveDispatchExecutionContext(incoming)).resolves.toEqual({
|
|
760
|
+
ownerEmail: "alice@example.test",
|
|
761
|
+
orgId: "org-managed",
|
|
762
|
+
principalType: "user",
|
|
763
|
+
installationId: "installation-managed",
|
|
764
|
+
});
|
|
765
|
+
expect(mocks.isOrgMember).toHaveBeenCalledWith(
|
|
766
|
+
"org-managed",
|
|
767
|
+
"alice@example.test",
|
|
768
|
+
);
|
|
769
|
+
expect(incoming.senderVerified).toBe(true);
|
|
770
|
+
expect(incoming.actorTrust).toEqual({
|
|
771
|
+
memberType: "member",
|
|
772
|
+
verified: true,
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("fails closed instead of borrowing the deployment owner when Slack hydration fails", async () => {
|
|
777
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
778
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
779
|
+
managedSlackInstallation(),
|
|
780
|
+
);
|
|
781
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
782
|
+
"managed-token",
|
|
783
|
+
);
|
|
784
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
785
|
+
new Response(JSON.stringify({ ok: false })),
|
|
786
|
+
);
|
|
787
|
+
const incoming = slackIncoming({
|
|
788
|
+
senderId: "U-MANAGED-UNKNOWN",
|
|
789
|
+
triggerKind: "dm",
|
|
790
|
+
conversationType: "dm",
|
|
791
|
+
platformContext: {
|
|
792
|
+
teamId: "T-MANAGED",
|
|
793
|
+
channelId: "D-MANAGED",
|
|
794
|
+
channelType: "im",
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const execution = await resolveDispatchExecutionContext(incoming);
|
|
799
|
+
expect(execution.ownerEmail).toMatch(/@integration\.local$/);
|
|
800
|
+
expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
|
|
801
|
+
expect(execution.orgId).toBeNull();
|
|
802
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
803
|
+
{
|
|
804
|
+
handled: true,
|
|
805
|
+
responseText:
|
|
806
|
+
"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("requires an explicit link when the verified identity is outside the installation org", async () => {
|
|
812
|
+
vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
|
|
813
|
+
vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
|
|
814
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
815
|
+
managedSlackInstallation(),
|
|
816
|
+
);
|
|
817
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
818
|
+
"managed-token",
|
|
819
|
+
);
|
|
820
|
+
mocks.isOrgMember.mockResolvedValueOnce(false);
|
|
821
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
822
|
+
new Response(
|
|
823
|
+
JSON.stringify({
|
|
824
|
+
ok: true,
|
|
825
|
+
user: { profile: { email: "outside@example.test" } },
|
|
826
|
+
}),
|
|
827
|
+
),
|
|
828
|
+
);
|
|
829
|
+
const incoming = slackIncoming({
|
|
830
|
+
senderId: "U-MANAGED-OUTSIDE",
|
|
831
|
+
triggerKind: "dm",
|
|
832
|
+
conversationType: "dm",
|
|
833
|
+
platformContext: {
|
|
834
|
+
teamId: "T-MANAGED",
|
|
835
|
+
channelId: "D-MANAGED",
|
|
836
|
+
channelType: "im",
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const execution = await resolveDispatchExecutionContext(incoming);
|
|
841
|
+
expect(execution.ownerEmail).toMatch(/@integration\.local$/);
|
|
842
|
+
expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
|
|
843
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
844
|
+
{
|
|
845
|
+
handled: true,
|
|
846
|
+
responseText:
|
|
847
|
+
"Agent Native is ready, but this Slack account is not linked to an Agent Native user yet. Open https://dispatch.agent-native.test/identities, create a Slack link token, then send `/link <token>` in this DM.",
|
|
848
|
+
},
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it("uses an explicitly linked in-org owner when the verified Slack email is outside the org", async () => {
|
|
853
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
854
|
+
managedSlackInstallation(),
|
|
855
|
+
);
|
|
856
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
857
|
+
"managed-token",
|
|
858
|
+
);
|
|
859
|
+
mocks.resolveLinkedOwner.mockResolvedValueOnce("member@example.test");
|
|
860
|
+
mocks.isOrgMember.mockImplementation(
|
|
861
|
+
async (_orgId, email) => email === "member@example.test",
|
|
862
|
+
);
|
|
863
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
864
|
+
new Response(
|
|
865
|
+
JSON.stringify({
|
|
866
|
+
ok: true,
|
|
867
|
+
user: { profile: { email: "outside@example.test" } },
|
|
868
|
+
}),
|
|
869
|
+
),
|
|
870
|
+
);
|
|
871
|
+
const incoming = slackIncoming({
|
|
872
|
+
senderId: "U-MANAGED-LINKED",
|
|
873
|
+
triggerKind: "dm",
|
|
874
|
+
conversationType: "dm",
|
|
875
|
+
platformContext: {
|
|
876
|
+
teamId: "T-MANAGED",
|
|
877
|
+
channelId: "D-MANAGED",
|
|
878
|
+
channelType: "im",
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
await expect(
|
|
883
|
+
resolveDispatchExecutionContext(incoming),
|
|
884
|
+
).resolves.toMatchObject({
|
|
885
|
+
ownerEmail: "member@example.test",
|
|
886
|
+
orgId: "org-managed",
|
|
887
|
+
principalType: "user",
|
|
888
|
+
});
|
|
889
|
+
expect(mocks.isOrgMember).toHaveBeenNthCalledWith(
|
|
890
|
+
1,
|
|
891
|
+
"org-managed",
|
|
892
|
+
"outside@example.test",
|
|
893
|
+
);
|
|
894
|
+
expect(mocks.isOrgMember).toHaveBeenNthCalledWith(
|
|
895
|
+
2,
|
|
896
|
+
"org-managed",
|
|
897
|
+
"member@example.test",
|
|
898
|
+
);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("denies a Slack Connect stranger before linked-owner resolution", async () => {
|
|
902
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
903
|
+
managedSlackInstallation(),
|
|
904
|
+
);
|
|
905
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
|
|
906
|
+
"managed-token",
|
|
907
|
+
);
|
|
908
|
+
mocks.resolveLinkedOwner.mockResolvedValueOnce("member@example.test");
|
|
909
|
+
mocks.isOrgMember.mockResolvedValueOnce(true);
|
|
910
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
|
911
|
+
new Response(
|
|
912
|
+
JSON.stringify({
|
|
913
|
+
ok: true,
|
|
914
|
+
user: {
|
|
915
|
+
is_stranger: true,
|
|
916
|
+
profile: { email: "stranger@example.test" },
|
|
917
|
+
},
|
|
918
|
+
}),
|
|
919
|
+
),
|
|
920
|
+
);
|
|
921
|
+
const incoming = slackIncoming({
|
|
922
|
+
senderId: "U-SLACK-CONNECT-STRANGER",
|
|
923
|
+
triggerKind: "dm",
|
|
924
|
+
conversationType: "dm",
|
|
925
|
+
platformContext: {
|
|
926
|
+
teamId: "T-MANAGED",
|
|
927
|
+
channelId: "D-MANAGED",
|
|
928
|
+
channelType: "im",
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const execution = await resolveDispatchExecutionContext(incoming);
|
|
933
|
+
expect(execution.orgId).toBeNull();
|
|
934
|
+
await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
|
|
935
|
+
{
|
|
936
|
+
handled: true,
|
|
937
|
+
responseText:
|
|
938
|
+
"This assistant is only available to members of this workspace's organization.",
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
expect(mocks.resolveLinkedOwner).not.toHaveBeenCalled();
|
|
942
|
+
expect(mocks.isOrgMember).not.toHaveBeenCalled();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("keeps a managed channel on its service principal even when Alice is verified", async () => {
|
|
946
|
+
mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
|
|
947
|
+
managedSlackInstallation(),
|
|
948
|
+
);
|
|
949
|
+
mocks.resolveSlackBotTokenForIncoming.mockResolvedValue("managed-token");
|
|
950
|
+
mocks.getIntegrationScope.mockResolvedValueOnce({
|
|
951
|
+
id: "scope-managed-channel",
|
|
952
|
+
serviceOwnerEmail: "scope-managed-channel@integration.local",
|
|
953
|
+
orgId: "org-managed",
|
|
954
|
+
defaultModel: null,
|
|
955
|
+
});
|
|
956
|
+
vi.mocked(globalThis.fetch)
|
|
957
|
+
.mockResolvedValueOnce(
|
|
958
|
+
new Response(
|
|
959
|
+
JSON.stringify({
|
|
960
|
+
ok: true,
|
|
961
|
+
user: {
|
|
962
|
+
profile: {
|
|
963
|
+
email: "alice@example.test",
|
|
964
|
+
display_name: "Alice",
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
}),
|
|
968
|
+
),
|
|
969
|
+
)
|
|
970
|
+
.mockResolvedValueOnce(
|
|
971
|
+
new Response(
|
|
972
|
+
JSON.stringify({ ok: true, channel: { is_ext_shared: false } }),
|
|
973
|
+
),
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
const execution = await resolveDispatchExecutionContext(
|
|
977
|
+
slackIncoming({
|
|
978
|
+
senderId: "U-MANAGED-ALICE",
|
|
979
|
+
triggerKind: "mention",
|
|
980
|
+
conversationType: "channel",
|
|
981
|
+
platformContext: {
|
|
982
|
+
teamId: "T-MANAGED",
|
|
983
|
+
channelId: "C-MANAGED",
|
|
984
|
+
channelType: "channel",
|
|
985
|
+
},
|
|
986
|
+
}),
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
expect(execution).toEqual({
|
|
990
|
+
ownerEmail: "scope-managed-channel@integration.local",
|
|
991
|
+
orgId: "org-managed",
|
|
992
|
+
principalType: "service",
|
|
993
|
+
installationId: "installation-managed",
|
|
994
|
+
scopeId: "scope-managed-channel",
|
|
426
995
|
});
|
|
996
|
+
expect(execution.ownerEmail).not.toBe("alice@example.test");
|
|
997
|
+
expect(mocks.isOrgMember).not.toHaveBeenCalled();
|
|
427
998
|
});
|
|
428
999
|
});
|