@wabot-dev/framework 0.9.80 → 2.0.0-beta.0

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 (77) hide show
  1. package/bin/skills.mjs +151 -0
  2. package/bin/wabot-skills.mjs +120 -0
  3. package/dist/build/build.js +1031 -8
  4. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
  5. package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
  6. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
  7. package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
  8. package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
  9. package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
  10. package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
  11. package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
  12. package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
  13. package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
  14. package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
  15. package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
  16. package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
  17. package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
  18. package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
  19. package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
  20. package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
  21. package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
  22. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
  23. package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
  24. package/dist/src/addon/ui/preact/outlet.js +22 -0
  25. package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
  26. package/dist/src/core/repository/CrudRepository.js +7 -7
  27. package/dist/src/feature/async/computeDedupKey.js +1 -1
  28. package/dist/src/feature/pg/@pgExtension.js +2 -4
  29. package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
  30. package/dist/src/feature/project-runner/scanner.js +1 -1
  31. package/dist/src/feature/repository/@memExtension.js +1 -2
  32. package/dist/src/feature/ui-controller/actions.js +35 -0
  33. package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
  34. package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
  35. package/dist/src/feature/ui-controller/bundler/index.js +4 -0
  36. package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
  37. package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
  38. package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
  39. package/dist/src/feature/ui-controller/document/escape.js +17 -0
  40. package/dist/src/feature/ui-controller/document/helpers.js +13 -0
  41. package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
  42. package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
  43. package/dist/src/feature/ui-controller/island/island.js +40 -0
  44. package/dist/src/feature/ui-controller/island/serialize.js +35 -0
  45. package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
  46. package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
  47. package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
  48. package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
  49. package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
  50. package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
  51. package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
  52. package/dist/src/index.d.ts +632 -3
  53. package/dist/src/index.js +30 -1
  54. package/dist/src/testing/index.d.ts +43 -1
  55. package/dist/src/testing/index.js +1 -0
  56. package/dist/src/testing/uiHarness.js +102 -0
  57. package/dist/src/ui/client.js +6 -0
  58. package/dist/src/ui/index.d.ts +427 -0
  59. package/dist/src/ui/index.js +29 -0
  60. package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
  61. package/dist/src/ui/jsx-dev-runtime.js +1 -0
  62. package/dist/src/ui/jsx-runtime.d.ts +1 -0
  63. package/dist/src/ui/jsx-runtime.js +1 -0
  64. package/package.json +33 -13
  65. package/skills/wabot-async/SKILL.md +143 -0
  66. package/skills/wabot-auth/SKILL.md +153 -0
  67. package/skills/wabot-chat/SKILL.md +140 -0
  68. package/skills/wabot-di-config/SKILL.md +117 -0
  69. package/skills/wabot-framework/SKILL.md +81 -0
  70. package/skills/wabot-framework/references/quickstart.md +85 -0
  71. package/skills/wabot-mindset/SKILL.md +159 -0
  72. package/skills/wabot-ops/SKILL.md +151 -0
  73. package/skills/wabot-persistence/SKILL.md +159 -0
  74. package/skills/wabot-rest-socket/SKILL.md +167 -0
  75. package/skills/wabot-testing/SKILL.md +214 -0
  76. package/skills/wabot-ui/SKILL.md +201 -0
  77. package/skills/wabot-validation/SKILL.md +108 -0
@@ -20,9 +20,7 @@ class InMemoryChatMemory {
20
20
  filePath;
21
21
  onActivity;
22
22
  constructor(chatId, onActivity) {
23
- this.filePath = chatId
24
- ? path.resolve(process.cwd(), PERSIST_DIR, `${chatId}.json`)
25
- : null;
23
+ this.filePath = chatId ? path.resolve(process.cwd(), PERSIST_DIR, `${chatId}.json`) : null;
26
24
  this.onActivity = onActivity ?? null;
27
25
  this.load();
28
26
  }
@@ -0,0 +1,180 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
3
+ import '../../../feature/chat-bot/ChatBot.js';
4
+ import '../../../feature/chat-bot/ChatOperator.js';
5
+ import '../../../feature/chat-bot/UnionChatAdapter.js';
6
+ import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
7
+ import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
8
+ import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
9
+ import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
10
+ import { singleton } from '../../../core/injection/index.js';
11
+ import 'uuid';
12
+ import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
13
+ import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
14
+ import '../../../core/error/setupErrorHandlers.js';
15
+ import { Logger } from '../../../core/logger/Logger.js';
16
+ import { Env } from '../../../core/env/Env.js';
17
+ import { OpenAI } from 'openai';
18
+
19
+ const XAI_SUPPORTED_IMAGE_MIME_TYPES = [
20
+ 'image/png',
21
+ 'image/jpeg',
22
+ 'image/webp',
23
+ 'image/gif',
24
+ ];
25
+ let XAIChatAdapter = class XAIChatAdapter {
26
+ env;
27
+ logger = new Logger('wabot:xai-chat-adapter');
28
+ client;
29
+ constructor(env) {
30
+ this.env = env;
31
+ this.client = new OpenAI({
32
+ apiKey: this.env.requireString('XAI_API_KEY'),
33
+ baseURL: 'https://api.x.ai/v1',
34
+ });
35
+ }
36
+ async nextItems(req) {
37
+ const messages = [];
38
+ if (req.systemPrompt) {
39
+ messages.push({ role: 'system', content: req.systemPrompt });
40
+ }
41
+ messages.push(...this.mapChatItems(req.prevItems));
42
+ const tools = req.tools.map((t) => this.mapTool(t));
43
+ const chatTools = tools.length > 0 ? tools : undefined;
44
+ let lastError;
45
+ for (const ref of req.models) {
46
+ try {
47
+ const result = await this.client.chat.completions.create({
48
+ model: ref.model,
49
+ messages,
50
+ tools: chatTools,
51
+ });
52
+ return this.mapResponse(result, ref.model);
53
+ }
54
+ catch (err) {
55
+ if (!isRetryableError(err))
56
+ throw err;
57
+ this.logger.warn(`xAI model '${ref.model}' failed with retryable error, trying next`, err instanceof Error ? { message: err.message } : { err });
58
+ lastError = err;
59
+ }
60
+ }
61
+ throw lastError ?? new Error('No xAI model could handle the request');
62
+ }
63
+ mapChatItems(items) {
64
+ const messages = [];
65
+ for (const item of items) {
66
+ switch (item.type) {
67
+ case 'humanMessage':
68
+ messages.push(this.mapHumanMessage(item.humanMessage));
69
+ break;
70
+ case 'botMessage':
71
+ messages.push(this.mapBotMessage(item.botMessage));
72
+ break;
73
+ case 'functionCall':
74
+ messages.push(...this.mapFunctionCall(item.functionCall));
75
+ break;
76
+ }
77
+ }
78
+ return messages;
79
+ }
80
+ mapHumanMessage(msg) {
81
+ if (isChatMessageEmpty(msg)) {
82
+ throw new Error('User message content is empty');
83
+ }
84
+ const content = [];
85
+ content.push({
86
+ type: 'text',
87
+ text: extractChatMessageText(msg, {
88
+ supportedImageMimeTypes: XAI_SUPPORTED_IMAGE_MIME_TYPES,
89
+ }),
90
+ });
91
+ if (msg.images) {
92
+ for (const image of msg.images) {
93
+ if (!XAI_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
94
+ continue;
95
+ content.push(this.mapImage(image));
96
+ }
97
+ }
98
+ return { role: 'user', content };
99
+ }
100
+ mapImage(image) {
101
+ const url = image.publicUrl ?? image.base64Url;
102
+ return { type: 'image_url', image_url: { url } };
103
+ }
104
+ mapBotMessage(msg) {
105
+ if (!msg.text)
106
+ throw new Error('Assistant message content is empty');
107
+ return { role: 'assistant', content: msg.text };
108
+ }
109
+ mapFunctionCall(fc) {
110
+ return [
111
+ {
112
+ role: 'assistant',
113
+ content: '',
114
+ tool_calls: [
115
+ {
116
+ id: fc.id,
117
+ type: 'function',
118
+ function: { name: fc.name, arguments: fc.arguments || '{}' },
119
+ },
120
+ ],
121
+ },
122
+ {
123
+ role: 'tool',
124
+ tool_call_id: fc.id,
125
+ content: String(fc.result ?? 'No result'),
126
+ },
127
+ ];
128
+ }
129
+ mapTool(tool) {
130
+ return {
131
+ type: 'function',
132
+ function: {
133
+ name: tool.name,
134
+ description: tool.description,
135
+ parameters: {
136
+ type: 'object',
137
+ properties: tool.parameters.reduce((prev, param) => ({ ...prev, [param.name]: param.schema }), {}),
138
+ required: tool.parameters.filter((p) => p.required).map((p) => p.name),
139
+ additionalProperties: false,
140
+ },
141
+ },
142
+ };
143
+ }
144
+ mapResponse(result, modelName) {
145
+ const nextItems = [];
146
+ const choice = result.choices[0];
147
+ const text = choice?.message.content?.trim();
148
+ if (text) {
149
+ nextItems.push({ type: 'botMessage', botMessage: { text } });
150
+ }
151
+ if (choice?.message.tool_calls?.length) {
152
+ for (const call of choice.message.tool_calls) {
153
+ if (call.type !== 'function')
154
+ continue;
155
+ nextItems.push({
156
+ type: 'functionCall',
157
+ functionCall: {
158
+ id: call.id ?? '',
159
+ name: call.function.name,
160
+ arguments: call.function.arguments,
161
+ },
162
+ });
163
+ }
164
+ }
165
+ const usage = {
166
+ inputTokens: result.usage?.prompt_tokens ?? 0,
167
+ outputTokens: result.usage?.completion_tokens ?? 0,
168
+ provider: 'xai',
169
+ model: result.model ?? modelName,
170
+ };
171
+ return { usage, nextItems };
172
+ }
173
+ };
174
+ XAIChatAdapter = __decorate([
175
+ chatAdapter({ provider: 'xai' }),
176
+ singleton(),
177
+ __metadata("design:paramtypes", [Env])
178
+ ], XAIChatAdapter);
179
+
180
+ export { XAIChatAdapter };
@@ -3,11 +3,7 @@ import * as path from 'node:path';
3
3
 
4
4
  function cmdChannelSocketPath() {
5
5
  if (process.platform === 'win32') {
6
- const cwdHash = crypto
7
- .createHash('sha1')
8
- .update(process.cwd())
9
- .digest('hex')
10
- .slice(0, 12);
6
+ const cwdHash = crypto.createHash('sha1').update(process.cwd()).digest('hex').slice(0, 12);
11
7
  return `\\\\.\\pipe\\wabot-cmd-channel-${cwdHash}`;
12
8
  }
13
9
  return path.resolve(process.cwd(), '.wabot/cmd-channel/socket');
@@ -0,0 +1,28 @@
1
+ import { container } from '../../../core/injection/index.js';
2
+ import { resolveConfigReferences } from '../../../core/config/resolver.js';
3
+ import { ControllerMetadataStore } from '../../../feature/chat-controller/metadata/ControllerMetadataStore.js';
4
+ import '../../../feature/chat-controller/ChatResolver.js';
5
+ import '../../../feature/chat-controller/runChatControllers.js';
6
+ import { HubSpotChannel } from './HubSpotChannel.js';
7
+ import { HubSpotChannelConfig } from './HubSpotChannelConfig.js';
8
+
9
+ function hubspot(config) {
10
+ return function (target, propertyKey) {
11
+ const resolved = resolveConfigReferences(config);
12
+ const store = container.resolve(ControllerMetadataStore);
13
+ store.saveChannelMetadata({
14
+ channelConstructor: HubSpotChannel,
15
+ functionName: propertyKey.toString(),
16
+ controllerConstructor: target.constructor,
17
+ channelConfig: new HubSpotChannelConfig({
18
+ accessToken: resolved.accessToken,
19
+ webhookSecret: resolved.webhookSecret,
20
+ webhookPath: resolved.webhookPath,
21
+ appId: resolved.appId,
22
+ senderActorId: resolved.senderActorId,
23
+ }),
24
+ });
25
+ };
26
+ }
27
+
28
+ export { hubspot };
@@ -0,0 +1,81 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import { injectable } from '../../../core/injection/index.js';
3
+ import { Env } from '../../../core/env/Env.js';
4
+ import { Logger } from '../../../core/logger/Logger.js';
5
+ import { hubspotChannelName } from './hubspotChannelName.js';
6
+ import { HubSpotChannelConfig } from './HubSpotChannelConfig.js';
7
+ import { HubSpotSender } from './HubSpotSender.js';
8
+ import { HubSpotReceiver } from './HubSpotReceiver.js';
9
+ import { markdownToChatHtml } from '../markdown/markdownToChatHtml.js';
10
+
11
+ var HubSpotChannel_1;
12
+ let HubSpotChannel = class HubSpotChannel {
13
+ static { HubSpotChannel_1 = this; }
14
+ static channelName = hubspotChannelName;
15
+ logger = new Logger(`wabot:hubspot-channel`);
16
+ sender;
17
+ receiver;
18
+ accessToken;
19
+ webhookSecret;
20
+ appId;
21
+ constructor(config, env) {
22
+ this.accessToken = config.accessToken ?? env.requireString('HUBSPOT_ACCESS_TOKEN');
23
+ this.webhookSecret = config.webhookSecret ?? env.requireString('HUBSPOT_WEBHOOK_SECRET');
24
+ this.appId = config.appId;
25
+ const resolvedConfig = new HubSpotChannelConfig({
26
+ accessToken: this.accessToken,
27
+ webhookSecret: this.webhookSecret,
28
+ webhookPath: config.webhookPath,
29
+ appId: this.appId,
30
+ senderActorId: config.senderActorId ?? process.env.HUBSPOT_SENDER_ACTOR_ID,
31
+ });
32
+ this.sender = new HubSpotSender(resolvedConfig);
33
+ this.receiver = new HubSpotReceiver(resolvedConfig);
34
+ }
35
+ listen(callback) {
36
+ this.receiver.listenMessage(async (payload) => {
37
+ await callback(this.toChannelMessage(payload, callback));
38
+ });
39
+ }
40
+ connect() {
41
+ this.receiver.connect();
42
+ }
43
+ disconnect() {
44
+ this.receiver.disconnect();
45
+ }
46
+ toChannelMessage(payload, callback) {
47
+ const chatConnection = {
48
+ id: payload.threadId,
49
+ chatType: 'PRIVATE',
50
+ channelName: HubSpotChannel_1.channelName,
51
+ };
52
+ return {
53
+ channel: hubspotChannelName,
54
+ chatConnection,
55
+ message: {
56
+ senderId: payload.senderId,
57
+ senderName: payload.senderName,
58
+ text: payload.text,
59
+ images: payload.files.length > 0 ? payload.files : undefined,
60
+ metadata: payload.metadata,
61
+ },
62
+ reply: async (replyMessage) => {
63
+ const text = replyMessage.text;
64
+ await this.sender.sendMessage({
65
+ threadId: payload.threadId,
66
+ text,
67
+ richText: text ? markdownToChatHtml(text) : undefined,
68
+ files: [...(replyMessage.images ?? []), ...(replyMessage.documents ?? [])],
69
+ channelId: payload.channelId,
70
+ channelAccountId: payload.channelAccountId,
71
+ });
72
+ },
73
+ };
74
+ }
75
+ };
76
+ HubSpotChannel = HubSpotChannel_1 = __decorate([
77
+ injectable(),
78
+ __metadata("design:paramtypes", [HubSpotChannelConfig, Env])
79
+ ], HubSpotChannel);
80
+
81
+ export { HubSpotChannel };
@@ -0,0 +1,20 @@
1
+ import { hubspotChannelName } from './hubspotChannelName.js';
2
+
3
+ class HubSpotChannelConfig {
4
+ channelName;
5
+ accessToken;
6
+ webhookSecret;
7
+ webhookPath;
8
+ appId;
9
+ senderActorId;
10
+ constructor(config) {
11
+ this.channelName = hubspotChannelName;
12
+ this.accessToken = config.accessToken;
13
+ this.webhookSecret = config.webhookSecret;
14
+ this.webhookPath = config.webhookPath ?? '/hubspot/webhook';
15
+ this.appId = config.appId;
16
+ this.senderActorId = config.senderActorId;
17
+ }
18
+ }
19
+
20
+ export { HubSpotChannelConfig };
@@ -0,0 +1,42 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import '../../../core/injection/index.js';
3
+ import '../../../feature/rest-controller/metadata/RestControllerMetadataStore.js';
4
+ import { restController } from '../../../feature/rest-controller/metadata/@restController.js';
5
+ import { runRestControllers } from '../../../feature/rest-controller/runRestControllers.js';
6
+ import { HubSpotWebhookController } from './HubSpotWebhookController.js';
7
+
8
+ class HubSpotReceiver {
9
+ config;
10
+ listener = null;
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ listenMessage(listener) {
15
+ this.listener = listener;
16
+ }
17
+ connect() {
18
+ const listener = this.listener;
19
+ if (!listener) {
20
+ throw new Error('HubSpotReceiver.connect() called before listenMessage()');
21
+ }
22
+ const webhookPath = this.config.webhookPath;
23
+ const webhookSecret = this.config.webhookSecret;
24
+ const accessToken = this.config.accessToken;
25
+ const channelName = this.config.channelName;
26
+ let UniqueController = class UniqueController extends HubSpotWebhookController {
27
+ constructor() {
28
+ super({ webhookSecret, accessToken, listener: listener, channelName });
29
+ }
30
+ };
31
+ UniqueController = __decorate([
32
+ restController(webhookPath),
33
+ __metadata("design:paramtypes", [])
34
+ ], UniqueController);
35
+ runRestControllers([UniqueController]);
36
+ }
37
+ disconnect() {
38
+ // No-op: the webhook is registered with Express and lives until the app shuts down.
39
+ }
40
+ }
41
+
42
+ export { HubSpotReceiver };
@@ -0,0 +1,118 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import { Client } from '@hubspot/api-client';
3
+ import { injectable } from '../../../core/injection/index.js';
4
+ import { Logger } from '../../../core/logger/Logger.js';
5
+ import { HubSpotChannelConfig } from './HubSpotChannelConfig.js';
6
+
7
+ let HubSpotSender = class HubSpotSender {
8
+ client;
9
+ accessToken;
10
+ baseUrl;
11
+ logger = new Logger('wabot:hubspot-sender');
12
+ defaultSenderActorId;
13
+ constructor(config, client) {
14
+ this.client = client ?? new Client({ accessToken: config.accessToken });
15
+ this.accessToken = config.accessToken;
16
+ this.baseUrl = 'https://api.hubapi.com';
17
+ this.defaultSenderActorId = config.senderActorId;
18
+ }
19
+ async sendMessage(req) {
20
+ const fileIds = [];
21
+ if (req.files && req.files.length > 0) {
22
+ for (const file of req.files) {
23
+ const uploaded = await this.uploadFile(file);
24
+ fileIds.push(uploaded.id);
25
+ }
26
+ }
27
+ const body = {
28
+ type: 'MESSAGE',
29
+ };
30
+ if (req.text)
31
+ body.text = req.text;
32
+ if (req.richText)
33
+ body.richText = req.richText;
34
+ if (fileIds.length > 0) {
35
+ body.attachments = fileIds.map((fileId) => ({ fileId }));
36
+ }
37
+ if (req.senderActorId)
38
+ body.senderActorId = req.senderActorId;
39
+ else if (this.defaultSenderActorId)
40
+ body.senderActorId = this.defaultSenderActorId;
41
+ if (req.channelId)
42
+ body.channelId = req.channelId;
43
+ if (req.channelAccountId)
44
+ body.channelAccountId = req.channelAccountId;
45
+ if (!body.text && !body.richText && (!body.attachments || body.attachments.length === 0)) {
46
+ throw new Error('HubSpot sendMessage requires at least text, richText or files');
47
+ }
48
+ const path = `/conversations/v3/conversations/threads/${encodeURIComponent(req.threadId)}/messages`;
49
+ const response = await this.client.apiRequest({
50
+ method: 'POST',
51
+ path,
52
+ body: JSON.stringify(body),
53
+ headers: { 'Content-Type': 'application/json' },
54
+ defaultJson: false,
55
+ });
56
+ if (!response.ok) {
57
+ const errorBody = await response.text();
58
+ this.logger.error(`HubSpot sendMessage failed for thread '${req.threadId}': ${response.status} ${errorBody}`);
59
+ throw new Error(`HubSpot sendMessage failed: ${response.status} ${errorBody}`);
60
+ }
61
+ const data = (await response.json());
62
+ if (!data.id) {
63
+ throw new Error('HubSpot sendMessage response did not include an id');
64
+ }
65
+ this.logger.trace(`HubSpot message sent to thread '${req.threadId}' as '${data.id}'`);
66
+ return { messageId: data.id };
67
+ }
68
+ async uploadFile(file) {
69
+ const httpFile = await toHttpFile(file);
70
+ const form = new FormData();
71
+ const fileBytes = new Uint8Array(httpFile.data);
72
+ form.append('file', new Blob([fileBytes]), httpFile.name);
73
+ form.append('options', JSON.stringify({ access: 'PUBLIC_INDEXABLE', overwrite: false }));
74
+ form.append('folderPath', '/');
75
+ const response = await fetch(`${this.baseUrl}/files/v3/files`, {
76
+ method: 'POST',
77
+ headers: { Authorization: `Bearer ${this.accessToken}` },
78
+ body: form,
79
+ });
80
+ if (!response.ok) {
81
+ const errorBody = await response.text();
82
+ this.logger.error(`HubSpot file upload failed for '${httpFile.name}': ${response.status} ${errorBody}`);
83
+ throw new Error(`HubSpot file upload failed: ${response.status} ${errorBody}`);
84
+ }
85
+ const data = (await response.json());
86
+ if (!data.id) {
87
+ throw new Error('HubSpot file upload response did not include an id');
88
+ }
89
+ return { id: String(data.id) };
90
+ }
91
+ };
92
+ HubSpotSender = __decorate([
93
+ injectable(),
94
+ __metadata("design:paramtypes", [HubSpotChannelConfig, Client])
95
+ ], HubSpotSender);
96
+ async function toHttpFile(file) {
97
+ const name = file.name ?? file.id;
98
+ if (file.base64Url) {
99
+ const data = decodeBase64Url(file.base64Url);
100
+ return { data, name };
101
+ }
102
+ if (file.publicUrl) {
103
+ const res = await fetch(file.publicUrl);
104
+ if (!res.ok) {
105
+ throw new Error(`Failed to download file from publicUrl: ${res.status}`);
106
+ }
107
+ const arrayBuffer = await res.arrayBuffer();
108
+ return { data: Buffer.from(arrayBuffer), name };
109
+ }
110
+ throw new Error('IChatMessageFile has neither base64Url nor publicUrl');
111
+ }
112
+ function decodeBase64Url(dataUri) {
113
+ const commaIdx = dataUri.indexOf(',');
114
+ const payload = commaIdx >= 0 ? dataUri.slice(commaIdx + 1) : dataUri;
115
+ return Buffer.from(payload, 'base64');
116
+ }
117
+
118
+ export { HubSpotSender };
@@ -0,0 +1,122 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import { IncomingMessage } from 'node:http';
3
+ import { CustomError } from '../../../core/error/CustomError.js';
4
+ import '../../../core/error/setupErrorHandlers.js';
5
+ import { Logger } from '../../../core/logger/Logger.js';
6
+ import '../../../core/injection/index.js';
7
+ import '../../../feature/rest-controller/metadata/RestControllerMetadataStore.js';
8
+ import { onPost } from '../../../feature/rest-controller/metadata/@onPost.js';
9
+ import { downloadHubSpotAttachments } from './downloadHubSpotAttachments.js';
10
+ import { verifyHubSpotSignatureV3 } from './verifyHubSpotSignatureV3.js';
11
+
12
+ const SIGNATURE_HEADER = 'x-hubspot-signature-v3';
13
+ const TIMESTAMP_HEADER = 'x-hubspot-request-timestamp';
14
+ class HubSpotWebhookController {
15
+ logger;
16
+ webhookSecret;
17
+ accessToken;
18
+ listener;
19
+ channelName;
20
+ constructor(deps) {
21
+ this.logger = new Logger(`wabot:hubspot-webhook:${deps.channelName}`);
22
+ this.webhookSecret = deps.webhookSecret;
23
+ this.accessToken = deps.accessToken;
24
+ this.listener = deps.listener;
25
+ this.channelName = deps.channelName;
26
+ }
27
+ async handleWebhook(req) {
28
+ const rawBody = await readRawBody(req);
29
+ const signature = headerValue(req, SIGNATURE_HEADER);
30
+ const timestamp = headerValue(req, TIMESTAMP_HEADER);
31
+ const requestUri = req.url ?? '/';
32
+ if (!signature || !timestamp) {
33
+ this.logger.warn('missing signature or timestamp header');
34
+ throw new CustomError({ httpCode: 401, message: 'missing signature headers' });
35
+ }
36
+ const ok = verifyHubSpotSignatureV3({
37
+ secret: this.webhookSecret,
38
+ method: req.method ?? 'POST',
39
+ url: requestUri,
40
+ rawBody,
41
+ timestampHeader: timestamp,
42
+ signatureHeader: signature,
43
+ });
44
+ if (!ok) {
45
+ this.logger.warn(`invalid signature for ${requestUri}`);
46
+ throw new CustomError({ httpCode: 401, message: 'invalid signature' });
47
+ }
48
+ let batch;
49
+ try {
50
+ batch = JSON.parse(rawBody);
51
+ }
52
+ catch (err) {
53
+ this.logger.warn(`failed to parse webhook body: ${err.message}`);
54
+ throw new CustomError({ httpCode: 400, message: 'invalid JSON body' });
55
+ }
56
+ if (!Array.isArray(batch)) {
57
+ throw new CustomError({ httpCode: 400, message: 'expected event batch array' });
58
+ }
59
+ for (const event of batch) {
60
+ await this.dispatch(event);
61
+ }
62
+ return null;
63
+ }
64
+ async dispatch(event) {
65
+ if (event.subscriptionType !== 'conversation.creation' &&
66
+ event.subscriptionType !== 'conversation.newMessage') {
67
+ this.logger.warn(`unhandled subscription type: ${event.subscriptionType ?? 'unknown'}`);
68
+ return;
69
+ }
70
+ const evt = event;
71
+ if (!evt.message || evt.message.direction !== 'INCOMING') {
72
+ return;
73
+ }
74
+ const msg = evt.message;
75
+ const files = await downloadHubSpotAttachments(msg.attachments, {
76
+ accessToken: this.accessToken,
77
+ logger: this.logger,
78
+ });
79
+ const payload = {
80
+ threadId: msg.threadId ?? evt.objectId ?? '',
81
+ messageId: msg.id,
82
+ senderId: msg.from?.actorId ?? 'unknown',
83
+ senderName: msg.from?.name,
84
+ channel: msg.channel,
85
+ channelId: msg.channelId,
86
+ channelAccountId: msg.channelAccountId,
87
+ text: msg.text,
88
+ files,
89
+ metadata: {
90
+ subscriptionType: event.subscriptionType,
91
+ portalId: String(evt.portalId ?? ''),
92
+ appId: evt.appId != null ? String(evt.appId) : '',
93
+ channelName: this.channelName,
94
+ },
95
+ };
96
+ await this.listener(payload);
97
+ }
98
+ }
99
+ __decorate([
100
+ onPost({ disableJsonParser: true, disableUrlEncodedParser: true }),
101
+ __metadata("design:type", Function),
102
+ __metadata("design:paramtypes", [IncomingMessage]),
103
+ __metadata("design:returntype", Promise)
104
+ ], HubSpotWebhookController.prototype, "handleWebhook", null);
105
+ function headerValue(req, name) {
106
+ const value = req.headers[name];
107
+ if (Array.isArray(value))
108
+ return value[0];
109
+ return value;
110
+ }
111
+ function readRawBody(req) {
112
+ return new Promise((resolve, reject) => {
113
+ let data = '';
114
+ req.on('data', (chunk) => {
115
+ data += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
116
+ });
117
+ req.on('end', () => resolve(data));
118
+ req.on('error', (err) => reject(err));
119
+ });
120
+ }
121
+
122
+ export { HubSpotWebhookController };
@@ -0,0 +1,45 @@
1
+ // Inline the HubSpot attachment bytes as `base64Url` data-URIs so downstream
2
+ // chat-bot consumers can forward them to LLM providers without HubSpot tokens
3
+ // or signed URLs. Mirrors the Telegram strategy in TelegramChannel.downloadChatFile.
4
+ async function downloadHubSpotAttachments(attachments, options) {
5
+ if (!attachments || attachments.length === 0)
6
+ return [];
7
+ const files = [];
8
+ for (const attachment of attachments) {
9
+ const file = await downloadOne(attachment, options);
10
+ if (file)
11
+ files.push(file);
12
+ }
13
+ return files;
14
+ }
15
+ async function downloadOne(attachment, options) {
16
+ const id = attachment.fileId ?? attachment.id;
17
+ const name = attachment.name;
18
+ const mimeType = attachment.mimeType ?? 'application/octet-stream';
19
+ if (!attachment.url) {
20
+ options.logger?.warn(`HubSpot attachment '${id}' has no url; skipping`);
21
+ return null;
22
+ }
23
+ try {
24
+ const response = await fetch(attachment.url, {
25
+ headers: { Authorization: `Bearer ${options.accessToken}` },
26
+ });
27
+ if (!response.ok) {
28
+ throw new Error(`${response.status} ${response.statusText}`);
29
+ }
30
+ const arrayBuffer = await response.arrayBuffer();
31
+ const base64 = Buffer.from(arrayBuffer).toString('base64');
32
+ return {
33
+ id,
34
+ name,
35
+ mimeType,
36
+ base64Url: `data:${mimeType};base64,${base64}`,
37
+ };
38
+ }
39
+ catch (err) {
40
+ options.logger?.warn(`failed to download HubSpot attachment '${id}': ${err.message}`);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export { downloadHubSpotAttachments };
@@ -0,0 +1,3 @@
1
+ const hubspotChannelName = 'HubSpotChannel';
2
+
3
+ export { hubspotChannelName };