@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.
@@ -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.75",
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
- "@axiom-lattice/agent-eval": "2.1.59",
43
- "@axiom-lattice/core": "2.1.65",
44
- "@axiom-lattice/pg-stores": "1.0.55",
45
- "@axiom-lattice/protocols": "2.1.33",
46
- "@axiom-lattice/queue-redis": "1.0.32"
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
- const envBackup = { ...process.env };
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(200);
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("skips lark route registration when lark config is disabled", async () => {
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(404);
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 installationConfig = {
6
- installationId: "install-1",
4
+ const installation = {
5
+ id: "install-1",
7
6
  tenantId: "tenant-a",
8
- assistantId: "assistant-a",
9
- appId: "cli_app_1",
10
- appSecret: "secret",
11
- verificationToken: "token-1",
12
- encryptKey: "encrypt-key",
13
- mappingMode: "hybrid" as const,
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 the challenge response during URL verification", async () => {
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
- createLarkEventHandler({
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(200);
49
- expect(response.json()).toEqual({ challenge: "challenge-token" });
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("handles one direct text message end to end", async () => {
42
+ it("returns 404 when installation channel is not lark", async () => {
55
43
  const app = fastify();
56
- const parseEvent = jest.fn().mockReturnValue({
57
- messageId: "om_1",
58
- openId: "ou_1",
59
- chatId: "oc_1",
60
- chatType: "direct",
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
- createLarkEventHandler({
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(200);
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("short-circuits when an inbound receipt is already processing", async () => {
67
+ it("returns 200 for non-message events (e.g. url verification)", async () => {
140
68
  const app = fastify();
141
- const parseEvent = jest.fn().mockReturnValue({
142
- messageId: "om_processing",
143
- openId: "ou_1",
144
- chatId: "oc_1",
145
- chatType: "direct",
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
- createLarkEventHandler({
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("rejects requests when verification fails", async () => {
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
- app.post(
202
- "/api/channels/lark/installations/:installationId/events",
203
- createLarkEventHandler({
204
- getInstallationConfig: jest.fn().mockResolvedValue(installationConfig),
205
- parseRequestBody: jest.fn().mockReturnValue({ header: { token: "bad" } }),
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
- createLarkEventHandler({
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
- type: "url_verification",
260
- challenge: "challenge-token",
261
- token: "token-1",
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(getInstallationConfig).toHaveBeenCalledWith("install-1");
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
  });