@axiom-lattice/gateway 3.0.2 → 3.0.3

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.
Files changed (155) hide show
  1. package/dist/index.js +31 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +31 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
@@ -1,30 +0,0 @@
1
- import type { FastifyInstance } from "fastify";
2
- import { registerLarkChannelRoutes } from "./lark/routes";
3
- import { registerWechatChannelRoutes } from "./wechat/routes";
4
- import type { MessageRouter } from "../router/MessageRouter";
5
- import type { ChannelInstallationStore } from "@axiom-lattice/protocols";
6
-
7
- interface ChannelRouteDependencies {
8
- router: MessageRouter;
9
- installationStore: ChannelInstallationStore;
10
- }
11
-
12
- type ChannelRouteRegistrar = (
13
- app: FastifyInstance,
14
- dependencies: ChannelRouteDependencies,
15
- ) => void;
16
-
17
- const channelRouteRegistrars: ChannelRouteRegistrar[] = [
18
- (app, deps) => registerLarkChannelRoutes(app, deps),
19
- (app, _deps) => registerWechatChannelRoutes(app),
20
- ];
21
-
22
- export function registerChannelRoutes(
23
- app: FastifyInstance,
24
- dependencies?: ChannelRouteDependencies,
25
- ): void {
26
- if (!dependencies) return;
27
- for (const registerRoutes of channelRouteRegistrars) {
28
- registerRoutes(app, dependencies);
29
- }
30
- }
@@ -1,294 +0,0 @@
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 { WechatChannelInstallationConfig } from "@axiom-lattice/protocols";
10
- import { getUpdates, sendMessage } from "./wechat-client";
11
- import { setContextToken, getContextToken, deleteContextToken } from "./context-store";
12
- import { Logger } from "../../logger/Logger";
13
-
14
- const logger = new Logger({ serviceName: "lattice/gateway/wechat" });
15
-
16
- const wechatConfigSchema = z.object({
17
- botToken: z.string(),
18
- uin: z.string().optional(),
19
- });
20
-
21
- const MAX_RECONNECT_DELAY_MS = 30_000;
22
- const BASE_RECONNECT_DELAY_MS = 1_000;
23
- const HEARTBEAT_INTERVAL_MS = 60_000;
24
- const CHANNEL_VERSION = "1.0.2";
25
-
26
- // ─── Message type constants ───────────────────────────────────────────────
27
-
28
- const MSG_TYPE_USER = 1;
29
- const MSG_ITEM_TEXT = 1;
30
-
31
- // ─── Polling connection state ─────────────────────────────────────────────
32
-
33
- interface PollingState {
34
- installationId: string;
35
- abortController: AbortController;
36
- lastActivity: number;
37
- heartbeatTimer: NodeJS.Timeout;
38
- }
39
-
40
- const activeConnections = new Map<string, PollingState>();
41
-
42
- // ─── Message dedup ────────────────────────────────────────────────────────
43
-
44
- const seenClientIds = new Set<string>();
45
-
46
- function addToDedup(clientId: string): boolean {
47
- if (!clientId || seenClientIds.has(clientId)) return false;
48
- seenClientIds.add(clientId);
49
- if (seenClientIds.size > 1000) {
50
- const first = seenClientIds.values().next().value;
51
- if (first !== undefined) seenClientIds.delete(first);
52
- }
53
- return true;
54
- }
55
-
56
- // ─── Text extraction from iLink message ───────────────────────────────────
57
-
58
- function extractText(msg: { item_list?: Array<{ type?: number; text_item?: { text?: string } }> }): string | null {
59
- if (!msg.item_list?.length) return null;
60
- for (const item of msg.item_list) {
61
- if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
62
- return item.text_item.text;
63
- }
64
- }
65
- return null;
66
- }
67
-
68
- // ─── Adapter ──────────────────────────────────────────────────────────────
69
-
70
- export const wechatChannelAdapter: ChannelAdapter<WechatChannelInstallationConfig> = {
71
- channel: "wechat",
72
-
73
- configSchema: wechatConfigSchema,
74
-
75
- async receive(
76
- rawPayload: unknown,
77
- installation: ChannelInstallation<WechatChannelInstallationConfig>,
78
- ): Promise<InboundMessage | null> {
79
- const msg = rawPayload as {
80
- from_user_id?: string;
81
- client_id?: string;
82
- message_type?: number;
83
- item_list?: Array<{ type?: number; text_item?: { text?: string } }>;
84
- context_token?: string;
85
- };
86
-
87
- // Only process user messages (type 1), skip bot echoes (type 2)
88
- if (msg.message_type !== MSG_TYPE_USER) return null;
89
-
90
- const senderId = msg.from_user_id;
91
- if (!senderId) return null;
92
-
93
- const text = extractText(msg);
94
- if (!text) return null;
95
-
96
- // Cache context token for reply
97
- if (msg.context_token) {
98
- setContextToken(installation.id, senderId, msg.context_token);
99
- }
100
-
101
- return {
102
- channel: "wechat",
103
- channelInstallationId: installation.id,
104
- tenantId: installation.tenantId,
105
- sender: {
106
- id: senderId,
107
- displayName: senderId.split("@")[0],
108
- },
109
- content: {
110
- text,
111
- },
112
- conversation: {
113
- id: senderId,
114
- type: "direct",
115
- },
116
- replyTarget: {
117
- adapterChannel: "wechat",
118
- channelInstallationId: installation.id,
119
- rawTarget: { senderId },
120
- },
121
- };
122
- },
123
-
124
- async sendReply(
125
- replyTarget: ReplyTarget,
126
- message: OutboundMessage,
127
- installation: ChannelInstallation<WechatChannelInstallationConfig>,
128
- ): Promise<void> {
129
- const senderId = replyTarget.rawTarget.senderId as string;
130
- const contextToken = getContextToken(installation.id, senderId);
131
-
132
- if (!contextToken) {
133
- logger.warn("WeChat context token expired, cannot send reply", {
134
- installationId: installation.id,
135
- senderId,
136
- });
137
- deleteContextToken(installation.id, senderId);
138
- return;
139
- }
140
-
141
- const { botToken } = installation.config;
142
- await sendMessage(botToken, senderId, message.text, contextToken);
143
- },
144
-
145
- resolveThreadId(message: InboundMessage, binding: unknown): string {
146
- const date = new Date().toISOString().split("T")[0];
147
- const agentId = (binding as { agentId: string }).agentId;
148
- return `wechat:dm:${message.sender.id}:${agentId}:${date}`;
149
- },
150
-
151
- async connect(
152
- installation: ChannelInstallation<WechatChannelInstallationConfig>,
153
- deps?: unknown,
154
- ): Promise<void> {
155
- const { id: installationId, tenantId, config } = installation;
156
-
157
- if (!config.botToken) {
158
- logger.warn("WeChat installation missing botToken, skipping", { installationId });
159
- return;
160
- }
161
-
162
- if (activeConnections.has(installationId)) {
163
- logger.warn("WeChat polling already running for installation, skipping", { installationId });
164
- return;
165
- }
166
-
167
- logger.info("WeChat polling starting", { installationId, tenantId });
168
-
169
- const abortController = new AbortController();
170
-
171
- const heartbeatTimer = setInterval(() => {
172
- const state2 = activeConnections.get(installationId);
173
- if (!state2) {
174
- clearInterval(heartbeatTimer);
175
- return;
176
- }
177
- const elapsed = Date.now() - state2.lastActivity;
178
- if (elapsed > HEARTBEAT_INTERVAL_MS * 2) {
179
- logger.error("WeChat polling heartbeat lost — no activity", {
180
- installationId,
181
- elapsedMs: elapsed,
182
- });
183
- state2.abortController.abort();
184
- }
185
- }, HEARTBEAT_INTERVAL_MS);
186
-
187
- const state: PollingState = {
188
- installationId,
189
- abortController,
190
- lastActivity: Date.now(),
191
- heartbeatTimer,
192
- };
193
- activeConnections.set(installationId, state);
194
-
195
- const router = (deps as { router?: { dispatch(msg: InboundMessage): Promise<unknown> } })?.router;
196
-
197
- let syncBuffer = "";
198
- let reconnectDelay = BASE_RECONNECT_DELAY_MS;
199
-
200
- const scheduleNextPoll = (): void => {
201
- poll().catch((err) => {
202
- logger.error("WeChat poll iteration crashed", {
203
- installationId,
204
- error: err instanceof Error ? err.message : String(err),
205
- });
206
- const currentState = activeConnections.get(installationId);
207
- if (currentState && !currentState.abortController.signal.aborted) {
208
- setTimeout(scheduleNextPoll, reconnectDelay);
209
- }
210
- });
211
- };
212
-
213
- const poll = async (): Promise<void> => {
214
- const currentState = activeConnections.get(installationId);
215
- if (!currentState || currentState.abortController.signal.aborted) {
216
- logger.info("WeChat polling aborted", { installationId });
217
- return;
218
- }
219
-
220
- try {
221
- const result = await getUpdates(
222
- config.botToken,
223
- syncBuffer,
224
- currentState.abortController.signal,
225
- );
226
- syncBuffer = result.syncBuffer;
227
- reconnectDelay = BASE_RECONNECT_DELAY_MS;
228
- currentState.lastActivity = Date.now();
229
-
230
- if (result.msgs.length > 0) {
231
- logger.info("WeChat poll received messages", {
232
- installationId,
233
- count: result.msgs.length,
234
- });
235
- }
236
-
237
- for (const rawMsg of result.msgs) {
238
- // Only user messages, skip bot echoes and non-text
239
- if (rawMsg.message_type !== MSG_TYPE_USER) continue;
240
-
241
- const text = extractText(rawMsg);
242
- if (!text) continue;
243
-
244
- if (!addToDedup((rawMsg as { client_id?: string }).client_id ?? "")) continue;
245
-
246
- const inbound = await wechatChannelAdapter.receive(
247
- rawMsg,
248
- installation as ChannelInstallation<WechatChannelInstallationConfig>,
249
- );
250
- if (inbound && router) {
251
- const dispatchResult = await router.dispatch(inbound);
252
- if (!(dispatchResult as { success?: boolean }).success) {
253
- logger.warn("WeChat dispatch failed", {
254
- installationId,
255
- error: (dispatchResult as { error?: { message?: string } }).error?.message,
256
- });
257
- }
258
- }
259
- }
260
- } catch (err) {
261
- if (err instanceof Error && err.name === "AbortError") {
262
- logger.info("WeChat poll aborted by signal", { installationId });
263
- return;
264
- }
265
-
266
- logger.error("WeChat poll error", {
267
- installationId,
268
- error: err instanceof Error ? err.message : String(err),
269
- });
270
- await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
271
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
272
- }
273
-
274
- scheduleNextPoll();
275
- };
276
-
277
- scheduleNextPoll();
278
- logger.info("WeChat polling started", { installationId });
279
- },
280
-
281
- async disconnect(installationId: string): Promise<void> {
282
- const state = activeConnections.get(installationId);
283
- if (!state) {
284
- logger.warn("WeChat polling not running, nothing to disconnect", { installationId });
285
- return;
286
- }
287
-
288
- logger.info("WeChat polling disconnecting", { installationId });
289
- state.abortController.abort();
290
- clearInterval(state.heartbeatTimer);
291
- activeConnections.delete(installationId);
292
- logger.info("WeChat polling disconnected", { installationId });
293
- },
294
- };
@@ -1,148 +0,0 @@
1
- import {
2
- setContextToken,
3
- getContextToken,
4
- deleteContextToken,
5
- getContextTokenStoreSize,
6
- } from "../context-store";
7
-
8
- describe("context-store", () => {
9
- beforeEach(() => {
10
- jest.useFakeTimers();
11
- // Clear all tokens by deleting known keys (no public clear API)
12
- });
13
-
14
- afterEach(() => {
15
- jest.useRealTimers();
16
- });
17
-
18
- describe("setContextToken / getContextToken", () => {
19
- it("stores and retrieves a token keyed by installationId + senderId", () => {
20
- setContextToken("inst-1", "user-a@im.wechat", "token-abc");
21
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("token-abc");
22
- });
23
-
24
- it("returns undefined for unknown installationId + senderId", () => {
25
- expect(getContextToken("inst-1", "unknown@im.wechat")).toBeUndefined();
26
- });
27
-
28
- it("returns undefined when senderId matches but installationId differs", () => {
29
- setContextToken("inst-1", "user-a@im.wechat", "token-abc");
30
- expect(getContextToken("inst-2", "user-a@im.wechat")).toBeUndefined();
31
- });
32
- });
33
-
34
- describe("isolation between installations", () => {
35
- it("does not collide when different installations use the same senderId", () => {
36
- setContextToken("inst-1", "user-x@im.wechat", "tok-111");
37
- setContextToken("inst-2", "user-x@im.wechat", "tok-222");
38
-
39
- expect(getContextToken("inst-1", "user-x@im.wechat")).toBe("tok-111");
40
- expect(getContextToken("inst-2", "user-x@im.wechat")).toBe("tok-222");
41
- });
42
-
43
- it("isolates tokens between multiple installations with multiple users", () => {
44
- setContextToken("inst-1", "alice@im.wechat", "a1");
45
- setContextToken("inst-1", "bob@im.wechat", "b1");
46
- setContextToken("inst-2", "alice@im.wechat", "a2");
47
- setContextToken("inst-2", "charlie@im.wechat", "c2");
48
-
49
- expect(getContextToken("inst-1", "alice@im.wechat")).toBe("a1");
50
- expect(getContextToken("inst-1", "bob@im.wechat")).toBe("b1");
51
- expect(getContextToken("inst-2", "alice@im.wechat")).toBe("a2");
52
- expect(getContextToken("inst-2", "charlie@im.wechat")).toBe("c2");
53
- });
54
- });
55
-
56
- describe("overwrite", () => {
57
- it("overwrites existing token for the same installationId + senderId", () => {
58
- setContextToken("inst-1", "user-a@im.wechat", "old-token");
59
- setContextToken("inst-1", "user-a@im.wechat", "new-token");
60
-
61
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("new-token");
62
- });
63
-
64
- it("does not affect other senderIds in the same installation", () => {
65
- setContextToken("inst-1", "user-a@im.wechat", "tok-a");
66
- setContextToken("inst-1", "user-b@im.wechat", "tok-b");
67
- setContextToken("inst-1", "user-a@im.wechat", "tok-a-v2");
68
-
69
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("tok-a-v2");
70
- expect(getContextToken("inst-1", "user-b@im.wechat")).toBe("tok-b");
71
- });
72
- });
73
-
74
- describe("deleteContextToken", () => {
75
- it("removes a specific token", () => {
76
- setContextToken("inst-1", "user-a@im.wechat", "tok-a");
77
- deleteContextToken("inst-1", "user-a@im.wechat");
78
-
79
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBeUndefined();
80
- });
81
-
82
- it("only removes the targeted key, leaving others intact", () => {
83
- setContextToken("inst-1", "alice@im.wechat", "tok-1");
84
- setContextToken("inst-1", "bob@im.wechat", "tok-2");
85
- setContextToken("inst-2", "alice@im.wechat", "tok-3");
86
-
87
- deleteContextToken("inst-1", "alice@im.wechat");
88
-
89
- expect(getContextToken("inst-1", "alice@im.wechat")).toBeUndefined();
90
- expect(getContextToken("inst-1", "bob@im.wechat")).toBe("tok-2");
91
- expect(getContextToken("inst-2", "alice@im.wechat")).toBe("tok-3");
92
- });
93
-
94
- it("is a no-op for unknown keys", () => {
95
- expect(() => deleteContextToken("inst-1", "no-such@im.wechat")).not.toThrow();
96
- });
97
- });
98
-
99
- describe("TTL expiry", () => {
100
- it("returns undefined after TTL has passed", () => {
101
- setContextToken("inst-1", "user-a@im.wechat", "tok-a");
102
-
103
- // Advance time past 24h TTL
104
- jest.advanceTimersByTime(25 * 60 * 60 * 1000);
105
-
106
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBeUndefined();
107
- });
108
-
109
- it("returns token within TTL window", () => {
110
- setContextToken("inst-1", "user-a@im.wechat", "tok-a");
111
-
112
- // Advance time to just under 24h
113
- jest.advanceTimersByTime(23 * 60 * 60 * 1000);
114
-
115
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("tok-a");
116
- });
117
-
118
- it("resets TTL when overwritten", () => {
119
- setContextToken("inst-1", "user-a@im.wechat", "tok-v1");
120
- jest.advanceTimersByTime(23 * 60 * 60 * 1000);
121
-
122
- // Overwrite resets the TTL
123
- setContextToken("inst-1", "user-a@im.wechat", "tok-v2");
124
- jest.advanceTimersByTime(23 * 60 * 60 * 1000);
125
-
126
- // Original TTL + 23h would have expired, but overwrite reset it
127
- expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("tok-v2");
128
- });
129
- });
130
-
131
- describe("getContextTokenStoreSize", () => {
132
- it("returns the number of stored entries", () => {
133
- const startSize = getContextTokenStoreSize();
134
-
135
- setContextToken("sz-1", "a@im.wechat", "x");
136
- expect(getContextTokenStoreSize()).toBe(startSize + 1);
137
-
138
- setContextToken("sz-1", "b@im.wechat", "y");
139
- expect(getContextTokenStoreSize()).toBe(startSize + 2);
140
-
141
- setContextToken("sz-2", "a@im.wechat", "z");
142
- expect(getContextTokenStoreSize()).toBe(startSize + 3);
143
-
144
- deleteContextToken("sz-1", "a@im.wechat");
145
- expect(getContextTokenStoreSize()).toBe(startSize + 2);
146
- });
147
- });
148
- });
@@ -1,54 +0,0 @@
1
- interface ContextTokenEntry {
2
- token: string;
3
- installationId: string;
4
- senderId: string;
5
- updatedAt: number;
6
- }
7
-
8
- const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
9
- const store = new Map<string, ContextTokenEntry>();
10
- const timers = new Map<string, NodeJS.Timeout>();
11
-
12
- function makeKey(installationId: string, senderId: string): string {
13
- return `${installationId}::${senderId}`;
14
- }
15
-
16
- export function getContextToken(installationId: string, senderId: string): string | undefined {
17
- const key = makeKey(installationId, senderId);
18
- const entry = store.get(key);
19
- if (!entry) return undefined;
20
- if (Date.now() - entry.updatedAt > TOKEN_TTL_MS) {
21
- deleteContextToken(installationId, senderId);
22
- return undefined;
23
- }
24
- return entry.token;
25
- }
26
-
27
- export function setContextToken(installationId: string, senderId: string, token: string): void {
28
- const key = makeKey(installationId, senderId);
29
-
30
- const existingTimer = timers.get(key);
31
- if (existingTimer) clearTimeout(existingTimer);
32
-
33
- store.set(key, { token, installationId, senderId, updatedAt: Date.now() });
34
-
35
- const timer = setTimeout(() => {
36
- store.delete(key);
37
- timers.delete(key);
38
- }, TOKEN_TTL_MS);
39
- timers.set(key, timer);
40
- }
41
-
42
- export function deleteContextToken(installationId: string, senderId: string): void {
43
- const key = makeKey(installationId, senderId);
44
- store.delete(key);
45
- const timer = timers.get(key);
46
- if (timer) {
47
- clearTimeout(timer);
48
- timers.delete(key);
49
- }
50
- }
51
-
52
- export function getContextTokenStoreSize(): number {
53
- return store.size;
54
- }
@@ -1,59 +0,0 @@
1
- import type { FastifyReply, FastifyRequest } from "fastify";
2
- import { getQrCode, getQrCodeStatus } from "./wechat-client";
3
- import type { QrSession } from "./types";
4
-
5
- const qrSessions = new Map<string, QrSession>();
6
- const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
7
-
8
- function cleanupExpiredSessions(): void {
9
- const now = Date.now();
10
- for (const [key, session] of qrSessions) {
11
- if (now - session.createdAt > SESSION_TTL_MS) {
12
- qrSessions.delete(key);
13
- }
14
- }
15
- }
16
-
17
- export async function handleGetQrCode(
18
- _request: FastifyRequest,
19
- reply: FastifyReply,
20
- ): Promise<void> {
21
- try {
22
- cleanupExpiredSessions();
23
- const { qrcode, qrcodeImgUrl } = await getQrCode();
24
- qrSessions.set(qrcode, { qrcode, createdAt: Date.now() });
25
- reply.send({ success: true, data: { qrcode, qrcodeImgUrl } });
26
- } catch (err) {
27
- reply.status(500).send({
28
- success: false,
29
- message: err instanceof Error ? err.message : "Failed to get QR code",
30
- });
31
- }
32
- }
33
-
34
- export async function handleGetQrCodeStatus(
35
- request: FastifyRequest,
36
- reply: FastifyReply,
37
- ): Promise<void> {
38
- const { qrcode } = request.query as { qrcode?: string };
39
- if (!qrcode) {
40
- reply.status(400).send({ success: false, message: "qrcode is required" });
41
- return;
42
- }
43
-
44
- const session = qrSessions.get(qrcode);
45
- if (!session) {
46
- reply.status(404).send({ success: false, message: "Session not found or expired" });
47
- return;
48
- }
49
-
50
- try {
51
- const result = await getQrCodeStatus(qrcode);
52
- reply.send({ success: true, data: result });
53
- } catch (err) {
54
- reply.status(500).send({
55
- success: false,
56
- message: err instanceof Error ? err.message : "Failed to check QR status",
57
- });
58
- }
59
- }
@@ -1,7 +0,0 @@
1
- import type { FastifyInstance } from "fastify";
2
- import { handleGetQrCode, handleGetQrCodeStatus } from "./controller";
3
-
4
- export function registerWechatChannelRoutes(app: FastifyInstance): void {
5
- app.get("/api/channels/wechat/setup/qrcode", handleGetQrCode);
6
- app.get("/api/channels/wechat/setup/status", handleGetQrCodeStatus);
7
- }
@@ -1,62 +0,0 @@
1
- export interface WechatInstallationConfig {
2
- botToken: string;
3
- uin?: string;
4
- }
5
-
6
- // ─── iLink message types ─────────────────────────────────────────────────
7
-
8
- export const MSG_TYPE_USER = 1;
9
- export const MSG_TYPE_BOT = 2;
10
- export const MSG_STATE_FINISH = 2;
11
- export const MSG_ITEM_TEXT = 1;
12
-
13
- export interface TextItem {
14
- text?: string;
15
- }
16
-
17
- export interface MessageItem {
18
- type?: number;
19
- text_item?: TextItem;
20
- }
21
-
22
- export interface WechatMessage {
23
- from_user_id?: string;
24
- to_user_id?: string;
25
- client_id?: string;
26
- message_type?: number;
27
- message_state?: number;
28
- item_list?: MessageItem[];
29
- context_token?: string;
30
- create_time_ms?: number;
31
- }
32
-
33
- export interface GetUpdatesResp {
34
- ret?: number;
35
- errcode?: number;
36
- errmsg?: string;
37
- msgs?: WechatMessage[];
38
- get_updates_buf?: string;
39
- longpolling_timeout_ms?: number;
40
- }
41
-
42
- // ─── QR login types ───────────────────────────────────────────────────────
43
-
44
- export interface WechatQrCodeResponse {
45
- qrcode?: string;
46
- qrcode_img_content?: string;
47
- error?: string;
48
- }
49
-
50
- export interface WechatQrCodeStatusResponse {
51
- status: "wait" | "scaned" | "confirmed" | "expired";
52
- bot_token?: string;
53
- ilink_bot_id?: string;
54
- ilink_user_id?: string;
55
- baseurl?: string;
56
- error?: string;
57
- }
58
-
59
- export interface QrSession {
60
- qrcode: string;
61
- createdAt: number;
62
- }