@axiom-lattice/gateway 2.1.74 → 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.
@@ -1,189 +1,47 @@
1
1
  import type { FastifyReply, FastifyRequest } from "fastify";
2
- import { parseLarkMessageEvent } from "./parser";
3
- import type {
4
- LarkIngressConfig,
5
- LarkUrlVerificationPayload,
6
- ParsedLarkMessageEvent,
7
- } from "./types";
8
-
9
- export interface LarkEventHandlerDependencies {
10
- getInstallationConfig: (
11
- installationId: string,
12
- ) => Promise<LarkIngressConfig | null>;
13
- parseRequestBody: (
14
- body: unknown,
15
- encryptKey?: string,
16
- ) => LarkUrlVerificationPayload;
17
- verifyParsedBody: (
18
- body: LarkUrlVerificationPayload,
19
- config: LarkIngressConfig,
20
- ) => boolean;
21
- parseEvent: (payload: unknown) => ParsedLarkMessageEvent | null;
22
- claimInboundReceipt: (input: {
23
- channel: string;
24
- channelAppId: string;
25
- externalMessageId: string;
26
- tenantId: string;
27
- }) => Promise<{ accepted: boolean; status: "processing" | "completed" }>;
28
- markInboundReceiptCompleted: (input: {
29
- channel: string;
30
- channelAppId: string;
31
- externalMessageId: string;
32
- tenantId: string;
33
- threadId: string;
34
- }) => Promise<void>;
35
- markInboundReceiptFailed: (input: {
36
- channel: string;
37
- channelAppId: string;
38
- externalMessageId: string;
39
- tenantId: string;
40
- }) => Promise<void>;
41
- resolveThread: (input: {
42
- channel: string;
43
- channelAppId: string;
44
- tenantId: string;
45
- assistantId: string;
46
- mappingMode: "user" | "group" | "hybrid";
47
- openId: string;
48
- chatId: string;
49
- chatType: "direct" | "group";
50
- messageId: string;
51
- workspaceId?: string;
52
- projectId?: string;
53
- }) => Promise<{ threadId: string }>;
54
- runAgentAndCollectText: (input: {
55
- tenantId: string;
56
- assistantId: string;
57
- threadId: string;
58
- text: string;
59
- workspaceId?: string;
60
- projectId?: string;
61
- }) => Promise<string>;
62
- sendTextReply: (input: {
63
- chatId: string;
64
- text: string;
65
- config: LarkIngressConfig;
66
- }) => Promise<void>;
67
- }
68
-
69
- export function createLarkEventHandler(
70
- dependencies: LarkEventHandlerDependencies,
71
- ) {
2
+ import type { ChannelInstallationStore, LarkChannelInstallationConfig } from "@axiom-lattice/protocols";
3
+ import { larkChannelAdapter } from "./LarkChannelAdapter";
4
+ import type { MessageRouter } from "../../router/MessageRouter";
5
+ import { parseLarkRequestBody } from "./verification";
6
+ import { Logger } from "../../logger/Logger";
7
+
8
+ const logger = new Logger({ serviceName: "lattice/gateway/lark" });
9
+
10
+ export function createLarkEventHandler(deps: {
11
+ installationStore: ChannelInstallationStore;
12
+ router: MessageRouter;
13
+ }) {
72
14
  return async function handleLarkEvent(
73
15
  request: FastifyRequest,
74
16
  reply: FastifyReply,
75
17
  ): Promise<void> {
76
- const installationId = (request.params as { installationId?: string })
77
- ?.installationId;
78
-
79
- if (!installationId) {
80
- reply.status(400).send({ success: false, message: "Missing installationId" });
81
- return;
82
- }
18
+ const { installationId } = request.params as { installationId: string };
83
19
 
84
- const config = await dependencies.getInstallationConfig(installationId);
85
- if (!config) {
86
- reply.status(404).send({ success: false, message: "Lark installation not found" });
20
+ const installation = await deps.installationStore.getInstallationById(installationId);
21
+ if (!installation || installation.channel !== "lark") {
22
+ reply.status(404).send({ success: false, message: "Installation not found" });
87
23
  return;
88
24
  }
89
25
 
90
- const body = dependencies.parseRequestBody(
91
- request.body,
92
- config.encryptKey,
93
- ) as LarkUrlVerificationPayload;
94
-
95
- if (!dependencies.verifyParsedBody(body, config)) {
96
- reply.status(401).send({ success: false, message: "Invalid Lark request" });
97
- return;
98
- }
26
+ const body = parseLarkRequestBody(request.body, installation.config.encryptKey);
99
27
 
100
28
  if (body.type === "url_verification" && body.challenge) {
101
29
  reply.status(200).send({ challenge: body.challenge });
102
30
  return;
103
31
  }
104
32
 
105
- const parsed = dependencies.parseEvent(request.body);
106
- if (!parsed) {
107
- reply.status(200).send({ success: true, ignored: true });
108
- return;
109
- }
110
-
111
- const receipt = await dependencies.claimInboundReceipt({
112
- channel: "lark",
113
- channelAppId: config.appId,
114
- externalMessageId: parsed.messageId,
115
- tenantId: config.tenantId,
116
- });
117
-
118
- if (!receipt.accepted) {
119
- reply.status(200).send(
120
- receipt.status === "processing"
121
- ? { success: true, processing: true }
122
- : { success: true, duplicate: true },
123
- );
33
+ const inboundMessage = await larkChannelAdapter.receive(request.body, installation);
34
+ if (!inboundMessage) {
35
+ reply.status(200).send();
124
36
  return;
125
37
  }
126
38
 
127
- try {
128
- const { threadId } = await dependencies.resolveThread({
129
- channel: "lark",
130
- channelAppId: config.appId,
131
- tenantId: config.tenantId,
132
- assistantId: config.assistantId,
133
- mappingMode: config.mappingMode,
134
- openId: parsed.openId,
135
- chatId: parsed.chatId,
136
- chatType: parsed.chatType,
137
- messageId: parsed.messageId,
138
- workspaceId: config.workspaceId,
139
- projectId: config.projectId,
140
- });
141
-
142
- const text = await dependencies.runAgentAndCollectText({
143
- tenantId: config.tenantId,
144
- assistantId: config.assistantId,
145
- threadId,
146
- text: parsed.text,
147
- workspaceId: config.workspaceId,
148
- projectId: config.projectId,
149
- });
150
-
151
- await dependencies.sendTextReply({
152
- chatId: parsed.chatId,
153
- text,
154
- config,
155
- });
156
-
157
- await dependencies.markInboundReceiptCompleted({
158
- channel: "lark",
159
- channelAppId: config.appId,
160
- externalMessageId: parsed.messageId,
161
- tenantId: config.tenantId,
162
- threadId,
39
+ deps.router.dispatch(inboundMessage).catch((error) => {
40
+ logger.error("Lark message dispatch error", {
41
+ error: error instanceof Error ? error.message : String(error),
163
42
  });
43
+ });
164
44
 
165
- reply.status(200).send({ success: true, threadId });
166
- } catch (error) {
167
- await dependencies.markInboundReceiptFailed({
168
- channel: "lark",
169
- channelAppId: config.appId,
170
- externalMessageId: parsed.messageId,
171
- tenantId: config.tenantId,
172
- });
173
- throw error;
174
- }
45
+ reply.status(200).send();
175
46
  };
176
47
  }
177
-
178
- export const handleLarkEvent = createLarkEventHandler({
179
- getInstallationConfig: async () => null,
180
- parseRequestBody: (body) => (body || {}) as LarkUrlVerificationPayload,
181
- verifyParsedBody: () => true,
182
- parseEvent: parseLarkMessageEvent,
183
- claimInboundReceipt: async () => ({ accepted: true, status: "processing" }),
184
- markInboundReceiptCompleted: async () => undefined,
185
- markInboundReceiptFailed: async () => undefined,
186
- resolveThread: async () => ({ threadId: "" }),
187
- runAgentAndCollectText: async () => "",
188
- sendTextReply: async () => undefined,
189
- });
@@ -1,121 +1,22 @@
1
1
  import type { FastifyInstance } from "fastify";
2
- import { getStoreLattice } from "@axiom-lattice/core";
3
- import {
4
- ChannelIdentityMappingStore,
5
- PostgreSQLChannelInstallationStore,
6
- } from "@axiom-lattice/pg-stores";
7
- import {
8
- createLarkEventHandler,
9
- type LarkEventHandlerDependencies,
10
- } from "./controller";
11
- import {
12
- isLarkIngressEnabled,
13
- loadLarkIngressConfig,
14
- } from "./config";
15
- import { createChannelThreadMappingService } from "./mapping-service";
16
- import { parseLarkMessageEvent } from "./parser";
17
- import { runAgentAndCollectLarkReply } from "./runner";
18
- import { createLarkSender } from "./sender";
19
- import { createLarkRequestVerifier, parseLarkRequestBody } from "./verification";
2
+ import { createLarkEventHandler } from "./controller";
3
+ import type { MessageRouter } from "../../router/MessageRouter";
4
+ import type { ChannelInstallationStore } from "@axiom-lattice/protocols";
20
5
 
21
6
  export function registerLarkChannelRoutes(
22
7
  app: FastifyInstance,
23
- dependencies?: LarkEventHandlerDependencies,
8
+ deps: {
9
+ installationStore: ChannelInstallationStore;
10
+ router: MessageRouter;
11
+ },
24
12
  ): void {
25
- const config = loadLarkIngressConfig();
26
- if (!dependencies && !isLarkIngressEnabled(config)) {
27
- return;
28
- }
29
- const handlerDependencies = dependencies || createDefaultLarkDependencies();
13
+ const handler = createLarkEventHandler({
14
+ installationStore: deps.installationStore,
15
+ router: deps.router,
16
+ });
30
17
 
31
18
  app.post(
32
19
  "/api/channels/lark/installations/:installationId/events",
33
- createLarkEventHandler({
34
- ...handlerDependencies,
35
- }),
20
+ handler,
36
21
  );
37
22
  }
38
-
39
- function createDefaultLarkDependencies(): LarkEventHandlerDependencies {
40
- const installationStore = new PostgreSQLChannelInstallationStore({
41
- poolConfig: getDatabaseUrl(),
42
- });
43
- const threadStore = getStoreLattice("default", "thread").store;
44
- const mappingStore = new ChannelIdentityMappingStore({
45
- poolConfig: getDatabaseUrl(),
46
- });
47
- const mappingService = createChannelThreadMappingService({
48
- mappingStore,
49
- threadStore,
50
- });
51
-
52
- return {
53
- getInstallationConfig: async (installationId) => {
54
- const installation = await installationStore.getInstallationById(
55
- installationId,
56
- );
57
-
58
- if (!installation || installation.channel !== "lark") {
59
- return null;
60
- }
61
-
62
- return {
63
- enabled: true,
64
- installationId: installation.id,
65
- tenantId: installation.tenantId,
66
- assistantId: installation.config.assistantId,
67
- appId: installation.config.appId,
68
- appSecret: installation.config.appSecret,
69
- verificationToken: installation.config.verificationToken,
70
- encryptKey: installation.config.encryptKey,
71
- workspaceId: installation.config.workspaceId,
72
- projectId: installation.config.projectId,
73
- mappingMode: installation.config.mappingMode,
74
- };
75
- },
76
- parseRequestBody: (body, encryptKey) => parseLarkRequestBody(body, encryptKey),
77
- verifyParsedBody: (body, config) => {
78
- if (!config.verificationToken) {
79
- return true;
80
- }
81
-
82
- return createLarkRequestVerifier(config)({
83
- body,
84
- } as unknown as Parameters<ReturnType<typeof createLarkRequestVerifier>>[0]);
85
- },
86
- parseEvent: parseLarkMessageEvent,
87
- claimInboundReceipt: (input) => mappingStore.claimInboundReceipt(input),
88
- markInboundReceiptCompleted: (input) =>
89
- mappingStore.markInboundReceiptCompleted(input),
90
- markInboundReceiptFailed: (input) =>
91
- mappingStore.markInboundReceiptFailed(input),
92
- resolveThread: (input) => mappingService.getOrCreateThread(input),
93
- runAgentAndCollectText: ({ tenantId, assistantId, threadId, text, workspaceId, projectId }) =>
94
- runAgentAndCollectLarkReply({
95
- tenantId,
96
- assistantId,
97
- threadId,
98
- text,
99
- workspaceId,
100
- projectId,
101
- }),
102
- sendTextReply: async ({ chatId, text, config }) => {
103
- const sender = await createLarkSender({
104
- appId: config.appId,
105
- appSecret: config.appSecret,
106
- });
107
-
108
- await sender.sendTextReply({ chatId, text });
109
- },
110
- };
111
- }
112
-
113
- function getDatabaseUrl(): string {
114
- const databaseUrl = process.env.DATABASE_URL;
115
-
116
- if (!databaseUrl) {
117
- throw new Error("DATABASE_URL is required for Lark channel ingress");
118
- }
119
-
120
- return databaseUrl;
121
- }
@@ -0,0 +1,20 @@
1
+ import type { ChannelAdapter } from "@axiom-lattice/protocols";
2
+
3
+ export class ChannelAdapterRegistry {
4
+ private adapters = new Map<string, ChannelAdapter>();
5
+
6
+ register(adapter: ChannelAdapter): void {
7
+ if (this.adapters.has(adapter.channel)) {
8
+ throw new Error(`Channel adapter "${adapter.channel}" already registered`);
9
+ }
10
+ this.adapters.set(adapter.channel, adapter);
11
+ }
12
+
13
+ get(channel: string): ChannelAdapter | undefined {
14
+ return this.adapters.get(channel);
15
+ }
16
+
17
+ list(): string[] {
18
+ return Array.from(this.adapters.keys());
19
+ }
20
+ }
@@ -1,9 +1,11 @@
1
1
  import type { FastifyInstance } from "fastify";
2
- import type { LarkEventHandlerDependencies } from "./lark/controller";
3
2
  import { registerLarkChannelRoutes } from "./lark/routes";
3
+ import type { MessageRouter } from "../router/MessageRouter";
4
+ import type { ChannelInstallationStore } from "@axiom-lattice/protocols";
4
5
 
5
6
  interface ChannelRouteDependencies {
6
- lark?: LarkEventHandlerDependencies;
7
+ router: MessageRouter;
8
+ installationStore: ChannelInstallationStore;
7
9
  }
8
10
 
9
11
  type ChannelRouteRegistrar = (
@@ -12,13 +14,14 @@ type ChannelRouteRegistrar = (
12
14
  ) => void;
13
15
 
14
16
  const channelRouteRegistrars: ChannelRouteRegistrar[] = [
15
- (app, dependencies) => registerLarkChannelRoutes(app, dependencies.lark),
17
+ (app, deps) => registerLarkChannelRoutes(app, deps),
16
18
  ];
17
19
 
18
20
  export function registerChannelRoutes(
19
21
  app: FastifyInstance,
20
- dependencies: ChannelRouteDependencies = {},
22
+ dependencies?: ChannelRouteDependencies,
21
23
  ): void {
24
+ if (!dependencies) return;
22
25
  for (const registerRoutes of channelRouteRegistrars) {
23
26
  registerRoutes(app, dependencies);
24
27
  }
@@ -0,0 +1,135 @@
1
+ import type { FastifyRequest, FastifyReply } from "fastify";
2
+ import type { CreateBindingInput } from "@axiom-lattice/protocols";
3
+ import { getBindingRegistry } from "../bindings";
4
+
5
+ function getTenantId(request: FastifyRequest): string {
6
+ const userTenantId = (request as any).user?.tenantId;
7
+ if (userTenantId) return userTenantId;
8
+ return (request.headers["x-tenant-id"] as string) || "default";
9
+ }
10
+
11
+ export async function getBindingList(
12
+ request: FastifyRequest<{
13
+ Querystring: {
14
+ channel?: string;
15
+ agentId?: string;
16
+ channelInstallationId?: string;
17
+ limit?: number;
18
+ offset?: number;
19
+ };
20
+ }>,
21
+ _reply: FastifyReply,
22
+ ) {
23
+ const tenantId = getTenantId(request);
24
+ const { channel, agentId, channelInstallationId, limit, offset } = request.query;
25
+ try {
26
+ const registry = getBindingRegistry();
27
+ const bindings = await registry.list({ channel, agentId, tenantId, channelInstallationId, limit, offset });
28
+ return { success: true, message: "Bindings retrieved", data: { records: bindings, total: bindings.length } };
29
+ } catch (error) {
30
+ console.error("Failed to get bindings:", error);
31
+ return { success: false, message: "Failed to retrieve bindings", data: { records: [], total: 0 } };
32
+ }
33
+ }
34
+
35
+ export async function getBinding(
36
+ request: FastifyRequest<{ Params: { id: string } }>,
37
+ reply: FastifyReply,
38
+ ) {
39
+ const tenantId = getTenantId(request);
40
+ try {
41
+ const registry = getBindingRegistry();
42
+ const bindings = await registry.list({ tenantId });
43
+ const binding = bindings.find((b) => b.id === request.params.id);
44
+ if (!binding || binding.tenantId !== tenantId) {
45
+ reply.status(404);
46
+ return { success: false, message: "Binding not found" };
47
+ }
48
+ return { success: true, message: "Binding retrieved", data: binding };
49
+ } catch (error) {
50
+ console.error("Failed to get binding:", error);
51
+ return { success: false, message: "Failed to retrieve binding" };
52
+ }
53
+ }
54
+
55
+ export async function createBinding(
56
+ request: FastifyRequest<{ Body: CreateBindingInput }>,
57
+ reply: FastifyReply,
58
+ ) {
59
+ const tenantId = getTenantId(request);
60
+ try {
61
+ const registry = getBindingRegistry();
62
+ const binding = await registry.create({ ...request.body, tenantId });
63
+ reply.status(201);
64
+ return { success: true, message: "Binding created", data: binding };
65
+ } catch (error) {
66
+ console.error("Failed to create binding:", error);
67
+ reply.status(500);
68
+ return { success: false, message: "Failed to create binding" };
69
+ }
70
+ }
71
+
72
+ export async function updateBinding(
73
+ request: FastifyRequest<{ Params: { id: string }; Body: Partial<CreateBindingInput & { enabled: boolean }> }>,
74
+ reply: FastifyReply,
75
+ ) {
76
+ try {
77
+ const tenantId = getTenantId(request);
78
+ const registry = getBindingRegistry();
79
+ const bindings = await registry.list({ tenantId });
80
+ const existing = bindings.find((b) => b.id === request.params.id);
81
+ if (!existing || existing.tenantId !== tenantId) {
82
+ reply.status(404);
83
+ return { success: false, message: "Binding not found" };
84
+ }
85
+ const binding = await registry.update(request.params.id, request.body);
86
+ return { success: true, message: "Binding updated", data: binding };
87
+ } catch (error) {
88
+ console.error("Failed to update binding:", error);
89
+ reply.status(500);
90
+ return { success: false, message: "Failed to update binding" };
91
+ }
92
+ }
93
+
94
+ export async function deleteBinding(
95
+ request: FastifyRequest<{ Params: { id: string } }>,
96
+ reply: FastifyReply,
97
+ ) {
98
+ try {
99
+ const tenantId = getTenantId(request);
100
+ const registry = getBindingRegistry();
101
+ const bindings = await registry.list({ tenantId });
102
+ const existing = bindings.find((b) => b.id === request.params.id);
103
+ if (!existing || existing.tenantId !== tenantId) {
104
+ reply.status(404);
105
+ return { success: false, message: "Binding not found" };
106
+ }
107
+ await registry.delete(request.params.id);
108
+ return { success: true, message: "Binding deleted" };
109
+ } catch (error) {
110
+ console.error("Failed to delete binding:", error);
111
+ reply.status(500);
112
+ return { success: false, message: "Failed to delete binding" };
113
+ }
114
+ }
115
+
116
+ export async function resolveBinding(
117
+ request: FastifyRequest<{
118
+ Querystring: { channel: string; senderId: string; channelInstallationId: string };
119
+ }>,
120
+ _reply: FastifyReply,
121
+ ) {
122
+ const tenantId = getTenantId(request);
123
+ const { channel, senderId, channelInstallationId } = request.query;
124
+ try {
125
+ const registry = getBindingRegistry();
126
+ const binding = await registry.resolve({ channel, senderId, channelInstallationId, tenantId });
127
+ if (!binding) {
128
+ return { success: false, message: "No binding found", data: null };
129
+ }
130
+ return { success: true, message: "Binding found", data: binding };
131
+ } catch (error) {
132
+ console.error("Failed to resolve binding:", error);
133
+ return { success: false, message: "Failed to resolve binding", data: null };
134
+ }
135
+ }
@@ -54,14 +54,17 @@ interface ChannelInstallationResponse {
54
54
  * Get channel installation store
55
55
  */
56
56
  async function getInstallationStore(): Promise<ChannelInstallationStore> {
57
- // Import dynamically to avoid circular dependencies
57
+ // Use registered store from StoreLatticeManager (supports both PG and in-memory)
58
+ const { getStoreLattice } = await import("@axiom-lattice/core");
59
+ const store = getStoreLattice("default", "channelInstallation").store as ChannelInstallationStore | undefined;
60
+ if (store) return store;
61
+
62
+ // Fallback: create PG store directly (legacy path)
58
63
  const { PostgreSQLChannelInstallationStore } = await import("@axiom-lattice/pg-stores");
59
64
  const databaseUrl = process.env.DATABASE_URL;
60
-
61
65
  if (!databaseUrl) {
62
66
  throw new Error("DATABASE_URL is required for channel installation store");
63
67
  }
64
-
65
68
  return new PostgreSQLChannelInstallationStore({
66
69
  poolConfig: databaseUrl,
67
70
  });
package/src/index.ts CHANGED
@@ -7,6 +7,13 @@ import staticPlugin from "@fastify/static";
7
7
  import path from "path";
8
8
  import { fileURLToPath } from "url";
9
9
  import { registerLatticeRoutes } from "./routes";
10
+ import type { ChannelInstallationStore, BindingRegistry } from "@axiom-lattice/protocols";
11
+ import { MessageRouter } from "./router/MessageRouter";
12
+ import { ChannelAdapterRegistry } from "./channels/registry";
13
+ import { createDeduplicationMiddleware, createRateLimitMiddleware, createAuditLoggerMiddleware } from "./router/middlewares";
14
+ import { setBindingRegistry } from "./bindings";
15
+ import { setBindingRegistry as setCoreBindingRegistry } from "@axiom-lattice/core";
16
+ import { larkChannelAdapter } from "./channels/lark/LarkChannelAdapter";
10
17
  import { extractUserFromAuthHeader } from "./controllers/auth";
11
18
  import { configureSwagger } from "./swagger";
12
19
  import {
@@ -265,8 +272,37 @@ const start = async (config?: LatticeGatewayConfig) => {
265
272
  // Access via: request.server.loggerLattice or app.loggerLattice
266
273
  app.decorate("loggerLattice", loggerLattice);
267
274
 
268
- // Register all routes
269
- registerLatticeRoutes(app);
275
+ // Channel infrastructure: use stores from StoreLatticeManager (defaults: InMemory).
276
+ // Projects can override with PG stores via registerStoreLattice before start().
277
+ let channelDeps: { router: MessageRouter; installationStore: ChannelInstallationStore } | undefined;
278
+ try {
279
+ const { getStoreLattice } = await import("@axiom-lattice/core");
280
+ const bindingStore = getStoreLattice("default", "channelBinding").store as BindingRegistry;
281
+ const installationStore = getStoreLattice("default", "channelInstallation").store as ChannelInstallationStore;
282
+ setBindingRegistry(bindingStore);
283
+ setCoreBindingRegistry(bindingStore);
284
+
285
+ const adapterRegistry = new ChannelAdapterRegistry();
286
+ adapterRegistry.register(larkChannelAdapter);
287
+
288
+ const router = new MessageRouter({
289
+ middlewares: [
290
+ createDeduplicationMiddleware(),
291
+ createRateLimitMiddleware(),
292
+ createAuditLoggerMiddleware(),
293
+ ],
294
+ bindingRegistry: bindingStore,
295
+ adapterRegistry,
296
+ installationStore,
297
+ });
298
+
299
+ channelDeps = { router, installationStore };
300
+ } catch {
301
+ // Stores not registered — channel routes will be skipped gracefully
302
+ }
303
+
304
+ // Register all routes (channel routes only active if channelDeps is set)
305
+ registerLatticeRoutes(app, channelDeps);
270
306
 
271
307
  // Set up database config store for on-demand loading
272
308
  try {
@@ -0,0 +1,14 @@
1
+ import type { InboundMessage, Binding } from "@axiom-lattice/protocols";
2
+
3
+ export interface MessageContext {
4
+ inboundMessage: InboundMessage;
5
+ binding?: Binding;
6
+ result?: string;
7
+ error?: Error;
8
+ metadata: Record<string, unknown>;
9
+ }
10
+
11
+ export type MessageMiddleware = (
12
+ ctx: MessageContext,
13
+ next: () => Promise<void>,
14
+ ) => Promise<void>;