@axiom-lattice/gateway 2.1.75 → 2.1.76
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/.turbo/turbo-build.log +14 -12
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +741 -401
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +689 -390
- package/dist/index.mjs.map +1 -1
- package/dist/sender-PX32VSHB.mjs +32 -0
- package/dist/sender-PX32VSHB.mjs.map +1 -0
- package/package.json +7 -6
- package/src/bindings/index.ts +14 -0
- package/src/channels/__tests__/routes.test.ts +23 -44
- package/src/channels/lark/LarkChannelAdapter.ts +75 -0
- package/src/channels/lark/__tests__/controller.test.ts +62 -196
- package/src/channels/lark/controller.ts +25 -167
- package/src/channels/lark/routes.ts +12 -111
- package/src/channels/registry.ts +20 -0
- package/src/channels/routes.ts +7 -4
- package/src/controllers/channel-bindings.ts +135 -0
- package/src/controllers/channel-installations.ts +6 -3
- package/src/index.ts +38 -2
- package/src/router/MessageContext.ts +14 -0
- package/src/router/MessageRouter.ts +201 -0
- package/src/router/middlewares/auditLogger.ts +34 -0
- package/src/router/middlewares/deduplication.ts +26 -0
- package/src/router/middlewares/index.ts +3 -0
- package/src/router/middlewares/rateLimit.ts +39 -0
- package/src/routes/channel-bindings.ts +11 -0
- package/src/routes/index.ts +50 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/channels/lark/sender.ts
|
|
2
|
+
async function createLarkSender(config, client) {
|
|
3
|
+
const resolved = client ?? await createDefaultLarkClient(config);
|
|
4
|
+
return {
|
|
5
|
+
async sendTextReply(input) {
|
|
6
|
+
const response = await resolved.im.v1.message.create({
|
|
7
|
+
params: {
|
|
8
|
+
receive_id_type: "chat_id"
|
|
9
|
+
},
|
|
10
|
+
data: {
|
|
11
|
+
receive_id: input.chatId,
|
|
12
|
+
msg_type: "text",
|
|
13
|
+
content: JSON.stringify({ text: input.text })
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
if (response.code && response.code !== 0) {
|
|
17
|
+
throw new Error("Failed to send Lark reply");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function createDefaultLarkClient(config) {
|
|
23
|
+
const Lark = await import("@larksuiteoapi/node-sdk");
|
|
24
|
+
return new Lark.Client({
|
|
25
|
+
appId: config.appId,
|
|
26
|
+
appSecret: config.appSecret
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
createLarkSender
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=sender-PX32VSHB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/channels/lark/sender.ts"],"sourcesContent":["interface LarkSenderConfig {\n appId: string;\n appSecret: string;\n}\n\ninterface LarkSdkMessageClient {\n im: {\n v1: {\n message: {\n create(input: {\n params: {\n receive_id_type: \"chat_id\";\n };\n data: {\n receive_id: string;\n msg_type: \"text\";\n content: string;\n };\n }): Promise<{ code?: number }>;\n };\n };\n };\n}\n\nexport async function createLarkSender(\n config: LarkSenderConfig,\n client?: LarkSdkMessageClient,\n) {\n const resolved = client ?? (await createDefaultLarkClient(config));\n\n return {\n async sendTextReply(input: { chatId: string; text: string }): Promise<void> {\n const response = await resolved.im.v1.message.create({\n params: {\n receive_id_type: \"chat_id\",\n },\n data: {\n receive_id: input.chatId,\n msg_type: \"text\",\n content: JSON.stringify({ text: input.text }),\n },\n });\n\n if (response.code && response.code !== 0) {\n throw new Error(\"Failed to send Lark reply\");\n }\n },\n };\n}\n\nasync function createDefaultLarkClient(config: LarkSenderConfig): Promise<LarkSdkMessageClient> {\n const Lark = await import(\"@larksuiteoapi/node-sdk\");\n\n return new Lark.Client({\n appId: config.appId,\n appSecret: config.appSecret,\n }) as LarkSdkMessageClient;\n}\n"],"mappings":";AAwBA,eAAsB,iBACpB,QACA,QACA;AACA,QAAM,WAAW,UAAW,MAAM,wBAAwB,MAAM;AAEhE,SAAO;AAAA,IACL,MAAM,cAAc,OAAwD;AAC1E,YAAM,WAAW,MAAM,SAAS,GAAG,GAAG,QAAQ,OAAO;AAAA,QACnD,QAAQ;AAAA,UACN,iBAAiB;AAAA,QACnB;AAAA,QACA,MAAM;AAAA,UACJ,YAAY,MAAM;AAAA,UAClB,UAAU;AAAA,UACV,SAAS,KAAK,UAAU,EAAE,MAAM,MAAM,KAAK,CAAC;AAAA,QAC9C;AAAA,MACF,CAAC;AAED,UAAI,SAAS,QAAQ,SAAS,SAAS,GAAG;AACxC,cAAM,IAAI,MAAM,2BAA2B;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAe,wBAAwB,QAAyD;AAC9F,QAAM,OAAO,MAAM,OAAO,yBAAyB;AAEnD,SAAO,IAAI,KAAK,OAAO;AAAA,IACrB,OAAO,OAAO;AAAA,IACd,WAAW,OAAO;AAAA,EACpB,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.76",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
"pg": "^8.11.0",
|
|
40
40
|
"redis": "^5.0.1",
|
|
41
41
|
"uuid": "^9.0.1",
|
|
42
|
-
"
|
|
43
|
-
"@axiom-lattice/
|
|
44
|
-
"@axiom-lattice/
|
|
45
|
-
"@axiom-lattice/
|
|
46
|
-
"@axiom-lattice/
|
|
42
|
+
"zod": "3.25.76",
|
|
43
|
+
"@axiom-lattice/agent-eval": "2.1.60",
|
|
44
|
+
"@axiom-lattice/core": "2.1.66",
|
|
45
|
+
"@axiom-lattice/pg-stores": "1.0.56",
|
|
46
|
+
"@axiom-lattice/protocols": "2.1.34",
|
|
47
|
+
"@axiom-lattice/queue-redis": "1.0.33"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/jest": "^29.5.14",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BindingRegistry } from "@axiom-lattice/protocols";
|
|
2
|
+
|
|
3
|
+
let registryInstance: BindingRegistry | null = null;
|
|
4
|
+
|
|
5
|
+
export function setBindingRegistry(registry: BindingRegistry): void {
|
|
6
|
+
registryInstance = registry;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getBindingRegistry(): BindingRegistry {
|
|
10
|
+
if (!registryInstance) {
|
|
11
|
+
throw new Error("BindingRegistry not initialized. Call setBindingRegistry() first.");
|
|
12
|
+
}
|
|
13
|
+
return registryInstance;
|
|
14
|
+
}
|
|
@@ -2,61 +2,40 @@ import fastify from "fastify";
|
|
|
2
2
|
import { registerChannelRoutes } from "../routes";
|
|
3
3
|
|
|
4
4
|
describe("registerChannelRoutes", () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
process.env = { ...envBackup };
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("registers the lark route dynamically", async () => {
|
|
5
|
+
it("skips registration when no dependencies are provided", async () => {
|
|
12
6
|
const app = fastify();
|
|
13
|
-
registerChannelRoutes(app
|
|
14
|
-
lark: {
|
|
15
|
-
getInstallationConfig: jest.fn().mockResolvedValue({
|
|
16
|
-
installationId: "install-1",
|
|
17
|
-
tenantId: "tenant-a",
|
|
18
|
-
assistantId: "assistant-a",
|
|
19
|
-
appId: "cli_app_1",
|
|
20
|
-
appSecret: "secret",
|
|
21
|
-
verificationToken: "token-1",
|
|
22
|
-
mappingMode: "hybrid",
|
|
23
|
-
}),
|
|
24
|
-
parseRequestBody: jest.fn().mockReturnValue({
|
|
25
|
-
type: "url_verification",
|
|
26
|
-
challenge: "challenge-token",
|
|
27
|
-
token: "token-1",
|
|
28
|
-
}),
|
|
29
|
-
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
30
|
-
parseEvent: jest.fn(),
|
|
31
|
-
claimInboundReceipt: jest.fn(),
|
|
32
|
-
markInboundReceiptCompleted: jest.fn(),
|
|
33
|
-
markInboundReceiptFailed: jest.fn(),
|
|
34
|
-
resolveThread: jest.fn(),
|
|
35
|
-
runAgentAndCollectText: jest.fn(),
|
|
36
|
-
sendTextReply: jest.fn(),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
7
|
+
registerChannelRoutes(app);
|
|
39
8
|
|
|
40
9
|
const response = await app.inject({
|
|
41
10
|
method: "POST",
|
|
42
11
|
url: "/api/channels/lark/installations/install-1/events",
|
|
43
|
-
payload: {
|
|
44
|
-
challenge: "challenge-token",
|
|
45
|
-
type: "url_verification",
|
|
46
|
-
},
|
|
12
|
+
payload: {},
|
|
47
13
|
});
|
|
48
14
|
|
|
49
|
-
expect(response.statusCode).toBe(
|
|
50
|
-
expect(response.json()).toEqual({ challenge: "challenge-token" });
|
|
15
|
+
expect(response.statusCode).toBe(404);
|
|
51
16
|
|
|
52
17
|
await app.close();
|
|
53
18
|
});
|
|
54
19
|
|
|
55
|
-
it("
|
|
56
|
-
process.env.LARK_ENABLED = "false";
|
|
57
|
-
|
|
20
|
+
it("registers the lark route when dependencies are provided", async () => {
|
|
58
21
|
const app = fastify();
|
|
59
|
-
registerChannelRoutes(app
|
|
22
|
+
registerChannelRoutes(app, {
|
|
23
|
+
router: {
|
|
24
|
+
dispatch: jest.fn().mockResolvedValue({ success: true }),
|
|
25
|
+
} as any,
|
|
26
|
+
installationStore: {
|
|
27
|
+
getInstallationById: jest.fn().mockResolvedValue({
|
|
28
|
+
id: "install-1",
|
|
29
|
+
tenantId: "tenant-a",
|
|
30
|
+
channel: "lark",
|
|
31
|
+
config: { appId: "cli_app_1", appSecret: "secret" },
|
|
32
|
+
enabled: true,
|
|
33
|
+
rejectWhenNoBinding: false,
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
updatedAt: new Date(),
|
|
36
|
+
}),
|
|
37
|
+
} as any,
|
|
38
|
+
});
|
|
60
39
|
|
|
61
40
|
const response = await app.inject({
|
|
62
41
|
method: "POST",
|
|
@@ -64,7 +43,7 @@ describe("registerChannelRoutes", () => {
|
|
|
64
43
|
payload: {},
|
|
65
44
|
});
|
|
66
45
|
|
|
67
|
-
expect(response.statusCode).toBe(
|
|
46
|
+
expect(response.statusCode).toBe(200);
|
|
68
47
|
|
|
69
48
|
await app.close();
|
|
70
49
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelAdapter,
|
|
4
|
+
InboundMessage,
|
|
5
|
+
OutboundMessage,
|
|
6
|
+
ReplyTarget,
|
|
7
|
+
ChannelInstallation,
|
|
8
|
+
} from "@axiom-lattice/protocols";
|
|
9
|
+
import type { LarkChannelInstallationConfig } from "@axiom-lattice/protocols";
|
|
10
|
+
import { parseLarkMessageEvent } from "./parser";
|
|
11
|
+
|
|
12
|
+
export const larkConfigSchema = z.object({
|
|
13
|
+
appId: z.string(),
|
|
14
|
+
appSecret: z.string(),
|
|
15
|
+
verificationToken: z.string().optional(),
|
|
16
|
+
encryptKey: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const larkChannelAdapter: ChannelAdapter<LarkChannelInstallationConfig> = {
|
|
20
|
+
channel: "lark",
|
|
21
|
+
|
|
22
|
+
configSchema: larkConfigSchema,
|
|
23
|
+
|
|
24
|
+
async receive(
|
|
25
|
+
rawPayload: unknown,
|
|
26
|
+
installation: ChannelInstallation<LarkChannelInstallationConfig>,
|
|
27
|
+
): Promise<InboundMessage | null> {
|
|
28
|
+
const event = parseLarkMessageEvent(rawPayload);
|
|
29
|
+
if (!event) return null;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
channel: "lark",
|
|
33
|
+
channelInstallationId: installation.id,
|
|
34
|
+
tenantId: installation.tenantId,
|
|
35
|
+
sender: {
|
|
36
|
+
id: event.openId,
|
|
37
|
+
displayName: undefined,
|
|
38
|
+
},
|
|
39
|
+
content: {
|
|
40
|
+
text: event.text,
|
|
41
|
+
metadata: {
|
|
42
|
+
chatId: event.chatId,
|
|
43
|
+
chatType: event.chatType,
|
|
44
|
+
messageId: event.messageId,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
conversation: {
|
|
48
|
+
id: event.chatId,
|
|
49
|
+
type: event.chatType,
|
|
50
|
+
},
|
|
51
|
+
replyTarget: {
|
|
52
|
+
adapterChannel: "lark",
|
|
53
|
+
channelInstallationId: installation.id,
|
|
54
|
+
rawTarget: {
|
|
55
|
+
chatId: event.chatId,
|
|
56
|
+
messageId: event.messageId,
|
|
57
|
+
chatType: event.chatType,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async sendReply(
|
|
64
|
+
replyTarget: ReplyTarget,
|
|
65
|
+
message: OutboundMessage,
|
|
66
|
+
installation: ChannelInstallation<LarkChannelInstallationConfig>,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
const { createLarkSender } = await import("./sender");
|
|
69
|
+
const sender = createLarkSender(installation.config);
|
|
70
|
+
await sender.sendTextReply({
|
|
71
|
+
chatId: replyTarget.rawTarget.chatId as string,
|
|
72
|
+
text: message.text,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -1,269 +1,135 @@
|
|
|
1
|
-
import type { ThreadStore } from "@axiom-lattice/protocols";
|
|
2
1
|
import fastify from "fastify";
|
|
3
2
|
import { createLarkEventHandler } from "../controller";
|
|
4
3
|
|
|
5
|
-
const
|
|
6
|
-
|
|
4
|
+
const installation = {
|
|
5
|
+
id: "install-1",
|
|
7
6
|
tenantId: "tenant-a",
|
|
8
|
-
|
|
9
|
-
appId: "cli_app_1",
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
channel: "lark" as const,
|
|
8
|
+
config: { appId: "cli_app_1", appSecret: "secret" },
|
|
9
|
+
enabled: true,
|
|
10
|
+
rejectWhenNoBinding: false,
|
|
11
|
+
createdAt: new Date(),
|
|
12
|
+
updatedAt: new Date(),
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
describe("createLarkEventHandler", () => {
|
|
17
|
-
it("returns
|
|
16
|
+
it("returns 404 when installation is not found", async () => {
|
|
18
17
|
const app = fastify();
|
|
18
|
+
const handler = createLarkEventHandler({
|
|
19
|
+
installationStore: {
|
|
20
|
+
getInstallationById: jest.fn().mockResolvedValue(null),
|
|
21
|
+
} as any,
|
|
22
|
+
router: { dispatch: jest.fn() } as any,
|
|
23
|
+
});
|
|
24
|
+
|
|
19
25
|
app.post(
|
|
20
26
|
"/api/channels/lark/installations/:installationId/events",
|
|
21
|
-
|
|
22
|
-
getInstallationConfig: jest.fn().mockResolvedValue(installationConfig),
|
|
23
|
-
parseRequestBody: jest.fn().mockReturnValue({
|
|
24
|
-
type: "url_verification",
|
|
25
|
-
challenge: "challenge-token",
|
|
26
|
-
token: "token-1",
|
|
27
|
-
}),
|
|
28
|
-
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
29
|
-
parseEvent: jest.fn(),
|
|
30
|
-
claimInboundReceipt: jest.fn(),
|
|
31
|
-
markInboundReceiptCompleted: jest.fn(),
|
|
32
|
-
markInboundReceiptFailed: jest.fn(),
|
|
33
|
-
resolveThread: jest.fn(),
|
|
34
|
-
runAgentAndCollectText: jest.fn(),
|
|
35
|
-
sendTextReply: jest.fn(),
|
|
36
|
-
}),
|
|
27
|
+
handler,
|
|
37
28
|
);
|
|
38
29
|
|
|
39
30
|
const response = await app.inject({
|
|
40
31
|
method: "POST",
|
|
41
32
|
url: "/api/channels/lark/installations/install-1/events",
|
|
42
|
-
payload: {
|
|
43
|
-
challenge: "challenge-token",
|
|
44
|
-
type: "url_verification",
|
|
45
|
-
},
|
|
33
|
+
payload: {},
|
|
46
34
|
});
|
|
47
35
|
|
|
48
|
-
expect(response.statusCode).toBe(
|
|
49
|
-
expect(response.json()).toEqual({
|
|
36
|
+
expect(response.statusCode).toBe(404);
|
|
37
|
+
expect(response.json()).toEqual({ success: false, message: "Installation not found" });
|
|
50
38
|
|
|
51
39
|
await app.close();
|
|
52
40
|
});
|
|
53
41
|
|
|
54
|
-
it("
|
|
42
|
+
it("returns 404 when installation channel is not lark", async () => {
|
|
55
43
|
const app = fastify();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
text: "hello",
|
|
44
|
+
const handler = createLarkEventHandler({
|
|
45
|
+
installationStore: {
|
|
46
|
+
getInstallationById: jest.fn().mockResolvedValue({ ...installation, channel: "slack" }),
|
|
47
|
+
} as any,
|
|
48
|
+
router: { dispatch: jest.fn() } as any,
|
|
62
49
|
});
|
|
63
|
-
const claimInboundReceipt = jest
|
|
64
|
-
.fn()
|
|
65
|
-
.mockResolvedValue({ accepted: true, status: "processing" });
|
|
66
|
-
const markInboundReceiptCompleted = jest.fn().mockResolvedValue(undefined);
|
|
67
|
-
const markInboundReceiptFailed = jest.fn().mockResolvedValue(undefined);
|
|
68
|
-
const resolveThread = jest.fn().mockResolvedValue({ threadId: "thread-1" });
|
|
69
|
-
const runAgentAndCollectText = jest.fn().mockResolvedValue("hi there");
|
|
70
|
-
const sendTextReply = jest.fn().mockResolvedValue(undefined);
|
|
71
50
|
|
|
72
51
|
app.post(
|
|
73
52
|
"/api/channels/lark/installations/:installationId/events",
|
|
74
|
-
|
|
75
|
-
getInstallationConfig: jest.fn().mockResolvedValue(installationConfig),
|
|
76
|
-
parseRequestBody: jest.fn().mockImplementation((body) => body),
|
|
77
|
-
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
78
|
-
parseEvent,
|
|
79
|
-
claimInboundReceipt,
|
|
80
|
-
markInboundReceiptCompleted,
|
|
81
|
-
markInboundReceiptFailed,
|
|
82
|
-
resolveThread,
|
|
83
|
-
runAgentAndCollectText,
|
|
84
|
-
sendTextReply,
|
|
85
|
-
}),
|
|
53
|
+
handler,
|
|
86
54
|
);
|
|
87
55
|
|
|
88
56
|
const response = await app.inject({
|
|
89
57
|
method: "POST",
|
|
90
58
|
url: "/api/channels/lark/installations/install-1/events",
|
|
91
|
-
payload: {
|
|
92
|
-
header: { event_type: "im.message.receive_v1" },
|
|
93
|
-
event: {
|
|
94
|
-
sender: { sender_id: { open_id: "ou_1" } },
|
|
95
|
-
message: {
|
|
96
|
-
message_id: "om_1",
|
|
97
|
-
chat_id: "oc_1",
|
|
98
|
-
chat_type: "p2p",
|
|
99
|
-
message_type: "text",
|
|
100
|
-
content: '{"text":"hello"}',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
59
|
+
payload: {},
|
|
104
60
|
});
|
|
105
61
|
|
|
106
|
-
expect(response.statusCode).toBe(
|
|
107
|
-
expect(claimInboundReceipt).toHaveBeenCalledWith({
|
|
108
|
-
channel: "lark",
|
|
109
|
-
channelAppId: "cli_app_1",
|
|
110
|
-
externalMessageId: "om_1",
|
|
111
|
-
tenantId: "tenant-a",
|
|
112
|
-
});
|
|
113
|
-
expect(resolveThread).toHaveBeenCalled();
|
|
114
|
-
expect(runAgentAndCollectText).toHaveBeenCalledWith({
|
|
115
|
-
assistantId: "assistant-a",
|
|
116
|
-
threadId: "thread-1",
|
|
117
|
-
text: "hello",
|
|
118
|
-
tenantId: "tenant-a",
|
|
119
|
-
workspaceId: undefined,
|
|
120
|
-
projectId: undefined,
|
|
121
|
-
});
|
|
122
|
-
expect(sendTextReply).toHaveBeenCalledWith({
|
|
123
|
-
chatId: "oc_1",
|
|
124
|
-
text: "hi there",
|
|
125
|
-
config: installationConfig,
|
|
126
|
-
});
|
|
127
|
-
expect(markInboundReceiptCompleted).toHaveBeenCalledWith({
|
|
128
|
-
channel: "lark",
|
|
129
|
-
channelAppId: "cli_app_1",
|
|
130
|
-
externalMessageId: "om_1",
|
|
131
|
-
tenantId: "tenant-a",
|
|
132
|
-
threadId: "thread-1",
|
|
133
|
-
});
|
|
134
|
-
expect(markInboundReceiptFailed).not.toHaveBeenCalled();
|
|
62
|
+
expect(response.statusCode).toBe(404);
|
|
135
63
|
|
|
136
64
|
await app.close();
|
|
137
65
|
});
|
|
138
66
|
|
|
139
|
-
it("
|
|
67
|
+
it("returns 200 for non-message events (e.g. url verification)", async () => {
|
|
140
68
|
const app = fastify();
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
text: "hello",
|
|
69
|
+
const handler = createLarkEventHandler({
|
|
70
|
+
installationStore: {
|
|
71
|
+
getInstallationById: jest.fn().mockResolvedValue(installation),
|
|
72
|
+
} as any,
|
|
73
|
+
router: { dispatch: jest.fn() } as any,
|
|
147
74
|
});
|
|
148
|
-
const claimInboundReceipt = jest
|
|
149
|
-
.fn()
|
|
150
|
-
.mockResolvedValue({ accepted: false, status: "processing" });
|
|
151
|
-
const resolveThread = jest.fn();
|
|
152
|
-
const runAgentAndCollectText = jest.fn();
|
|
153
|
-
const sendTextReply = jest.fn();
|
|
154
75
|
|
|
155
76
|
app.post(
|
|
156
77
|
"/api/channels/lark/installations/:installationId/events",
|
|
157
|
-
|
|
158
|
-
getInstallationConfig: jest.fn().mockResolvedValue(installationConfig),
|
|
159
|
-
parseRequestBody: jest.fn().mockImplementation((body) => body),
|
|
160
|
-
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
161
|
-
parseEvent,
|
|
162
|
-
claimInboundReceipt,
|
|
163
|
-
markInboundReceiptCompleted: jest.fn(),
|
|
164
|
-
markInboundReceiptFailed: jest.fn(),
|
|
165
|
-
resolveThread,
|
|
166
|
-
runAgentAndCollectText,
|
|
167
|
-
sendTextReply,
|
|
168
|
-
}),
|
|
78
|
+
handler,
|
|
169
79
|
);
|
|
170
80
|
|
|
171
81
|
const response = await app.inject({
|
|
172
82
|
method: "POST",
|
|
173
83
|
url: "/api/channels/lark/installations/install-1/events",
|
|
174
|
-
payload: {
|
|
175
|
-
header: { event_type: "im.message.receive_v1" },
|
|
176
|
-
event: {
|
|
177
|
-
sender: { sender_id: { open_id: "ou_1" } },
|
|
178
|
-
message: {
|
|
179
|
-
message_id: "om_processing",
|
|
180
|
-
chat_id: "oc_1",
|
|
181
|
-
chat_type: "p2p",
|
|
182
|
-
message_type: "text",
|
|
183
|
-
content: '{"text":"hello"}',
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
84
|
+
payload: { type: "url_verification", challenge: "challenge-token" },
|
|
187
85
|
});
|
|
188
86
|
|
|
189
87
|
expect(response.statusCode).toBe(200);
|
|
190
|
-
expect(response.json()).toEqual({ success: true, processing: true });
|
|
191
|
-
expect(resolveThread).not.toHaveBeenCalled();
|
|
192
|
-
expect(runAgentAndCollectText).not.toHaveBeenCalled();
|
|
193
|
-
expect(sendTextReply).not.toHaveBeenCalled();
|
|
194
88
|
|
|
195
89
|
await app.close();
|
|
196
90
|
});
|
|
197
91
|
|
|
198
|
-
it("
|
|
92
|
+
it("dispatches valid message events via router", async () => {
|
|
199
93
|
const app = fastify();
|
|
94
|
+
const dispatch = jest.fn().mockResolvedValue({ success: true });
|
|
200
95
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
verifyParsedBody: jest.fn().mockReturnValue(false),
|
|
207
|
-
parseEvent: jest.fn(),
|
|
208
|
-
claimInboundReceipt: jest.fn(),
|
|
209
|
-
markInboundReceiptCompleted: jest.fn(),
|
|
210
|
-
markInboundReceiptFailed: jest.fn(),
|
|
211
|
-
resolveThread: jest.fn(),
|
|
212
|
-
runAgentAndCollectText: jest.fn(),
|
|
213
|
-
sendTextReply: jest.fn(),
|
|
214
|
-
}),
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const response = await app.inject({
|
|
218
|
-
method: "POST",
|
|
219
|
-
url: "/api/channels/lark/installations/install-1/events",
|
|
220
|
-
payload: {
|
|
221
|
-
header: { event_type: "im.message.receive_v1" },
|
|
222
|
-
},
|
|
96
|
+
const handler = createLarkEventHandler({
|
|
97
|
+
installationStore: {
|
|
98
|
+
getInstallationById: jest.fn().mockResolvedValue(installation),
|
|
99
|
+
} as any,
|
|
100
|
+
router: { dispatch } as any,
|
|
223
101
|
});
|
|
224
102
|
|
|
225
|
-
expect(response.statusCode).toBe(401);
|
|
226
|
-
expect(response.json()).toEqual({ success: false, message: "Invalid Lark request" });
|
|
227
|
-
|
|
228
|
-
await app.close();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("loads installation-scoped config from the route parameter", async () => {
|
|
232
|
-
const app = fastify();
|
|
233
|
-
const getInstallationConfig = jest.fn().mockResolvedValue(installationConfig);
|
|
234
|
-
|
|
235
103
|
app.post(
|
|
236
104
|
"/api/channels/lark/installations/:installationId/events",
|
|
237
|
-
|
|
238
|
-
getInstallationConfig,
|
|
239
|
-
parseRequestBody: jest.fn().mockReturnValue({
|
|
240
|
-
type: "url_verification",
|
|
241
|
-
challenge: "challenge-token",
|
|
242
|
-
token: "token-1",
|
|
243
|
-
}),
|
|
244
|
-
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
245
|
-
parseEvent: jest.fn(),
|
|
246
|
-
claimInboundReceipt: jest.fn(),
|
|
247
|
-
markInboundReceiptCompleted: jest.fn(),
|
|
248
|
-
markInboundReceiptFailed: jest.fn(),
|
|
249
|
-
resolveThread: jest.fn(),
|
|
250
|
-
runAgentAndCollectText: jest.fn(),
|
|
251
|
-
sendTextReply: jest.fn(),
|
|
252
|
-
}),
|
|
105
|
+
handler,
|
|
253
106
|
);
|
|
254
107
|
|
|
255
108
|
const response = await app.inject({
|
|
256
109
|
method: "POST",
|
|
257
110
|
url: "/api/channels/lark/installations/install-1/events",
|
|
258
111
|
payload: {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
112
|
+
header: { event_type: "im.message.receive_v1" },
|
|
113
|
+
event: {
|
|
114
|
+
sender: { sender_id: { open_id: "ou_1" } },
|
|
115
|
+
message: {
|
|
116
|
+
message_id: "om_1",
|
|
117
|
+
chat_id: "oc_1",
|
|
118
|
+
chat_type: "p2p",
|
|
119
|
+
message_type: "text",
|
|
120
|
+
content: '{"text":"hello"}',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
262
123
|
},
|
|
263
124
|
});
|
|
264
125
|
|
|
265
126
|
expect(response.statusCode).toBe(200);
|
|
266
|
-
expect(
|
|
127
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(dispatch.mock.calls[0][0]).toMatchObject({
|
|
129
|
+
channel: "lark",
|
|
130
|
+
channelInstallationId: "install-1",
|
|
131
|
+
tenantId: "tenant-a",
|
|
132
|
+
});
|
|
267
133
|
|
|
268
134
|
await app.close();
|
|
269
135
|
});
|