commune-ai 0.1.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.
- package/README.md +245 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.js +153 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/listener.d.ts +15 -0
- package/dist/listener.js +85 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +1 -0
- package/dist/webhooks.d.ts +2 -0
- package/dist/webhooks.js +37 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# commune-ai
|
|
2
|
+
|
|
3
|
+
**Email infrastructure for AI agents**
|
|
4
|
+
|
|
5
|
+
Build agents that receive emails, reply in threads, access conversation history, and search across all organizational emails. Most teams get their first agent responding in **~15 minutes**.
|
|
6
|
+
|
|
7
|
+
## What you get
|
|
8
|
+
|
|
9
|
+
1. **Webhook Integration** - Receive emails instantly as structured data
|
|
10
|
+
2. **Thread-aware Replies** - Reply in email conversations with full context
|
|
11
|
+
3. **User History** - Access complete conversation history per user
|
|
12
|
+
4. **Organization Search** - Semantic search across all emails (coming soon)
|
|
13
|
+
|
|
14
|
+
> By default, the SDK talks to the hosted Commune API. If you self‑host,
|
|
15
|
+
> pass `baseUrl` to the client.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
```bash
|
|
21
|
+
npm install commune-ai
|
|
22
|
+
# or
|
|
23
|
+
yarn add commune-ai
|
|
24
|
+
# or
|
|
25
|
+
pnpm add commune-ai
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Receive emails instantly
|
|
31
|
+
|
|
32
|
+
Get emails delivered to your agent as structured webhook data.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import express from "express";
|
|
36
|
+
import { CommuneClient, createWebhookHandler } from "commune-ai";
|
|
37
|
+
|
|
38
|
+
const client = new CommuneClient({
|
|
39
|
+
apiKey: process.env.COMMUNE_API_KEY,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const handler = createWebhookHandler({
|
|
43
|
+
onEvent: async (message, context) => {
|
|
44
|
+
// Every email arrives as structured data:
|
|
45
|
+
// message = {
|
|
46
|
+
// channel: "email",
|
|
47
|
+
// conversation_id: "thread_id",
|
|
48
|
+
// participants: [{ role: "sender", identity: "user@example.com" }],
|
|
49
|
+
// content: "Can you help with pricing?",
|
|
50
|
+
// metadata: { subject: "Pricing Question", ... }
|
|
51
|
+
// }
|
|
52
|
+
|
|
53
|
+
console.log(`New email: ${message.content}`);
|
|
54
|
+
|
|
55
|
+
// Your agent logic here
|
|
56
|
+
const reply = `Thanks for your email! I'll help with that.`;
|
|
57
|
+
|
|
58
|
+
// Reply in the same email thread
|
|
59
|
+
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
60
|
+
if (sender) {
|
|
61
|
+
await client.messages.send({
|
|
62
|
+
to: sender,
|
|
63
|
+
text: reply,
|
|
64
|
+
conversation_id: message.conversation_id,
|
|
65
|
+
domainId: context.payload.domainId,
|
|
66
|
+
inboxId: context.payload.inboxId,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const app = express();
|
|
73
|
+
app.post("/webhook", express.raw({ type: "*/*" }), handler);
|
|
74
|
+
app.listen(3000, () => console.log("Agent listening on :3000"));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Email structure:**
|
|
78
|
+
```ts
|
|
79
|
+
interface UnifiedMessage {
|
|
80
|
+
channel: "email";
|
|
81
|
+
message_id: string;
|
|
82
|
+
conversation_id: string; // email thread ID
|
|
83
|
+
participants: Array<{
|
|
84
|
+
role: "sender" | "to" | "cc" | "bcc";
|
|
85
|
+
identity: string; // email address
|
|
86
|
+
}>;
|
|
87
|
+
content: string;
|
|
88
|
+
content_html?: string;
|
|
89
|
+
metadata: {
|
|
90
|
+
subject?: string;
|
|
91
|
+
created_at: string;
|
|
92
|
+
// ... more email metadata
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Reply in email threads
|
|
100
|
+
|
|
101
|
+
Keep conversations organized by replying in the same email thread.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import express from "express";
|
|
105
|
+
import { CommuneClient, createWebhookHandler } from "commune-ai";
|
|
106
|
+
|
|
107
|
+
const client = new CommuneClient({
|
|
108
|
+
apiKey: process.env.COMMUNE_API_KEY,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const handler = createWebhookHandler({
|
|
112
|
+
onEvent: async (message, context) => {
|
|
113
|
+
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
114
|
+
if (!sender) return;
|
|
115
|
+
|
|
116
|
+
// Reply maintains the email thread
|
|
117
|
+
await client.messages.send({
|
|
118
|
+
to: sender,
|
|
119
|
+
text: "Thanks for your question! Here's what I can help with...",
|
|
120
|
+
conversation_id: message.conversation_id, // keeps it in thread
|
|
121
|
+
subject: `Re: ${message.metadata.subject}`, // thread subject
|
|
122
|
+
domainId: context.payload.domainId,
|
|
123
|
+
inboxId: context.payload.inboxId,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const app = express();
|
|
129
|
+
app.post("/webhook", express.raw({ type: "*/*" }), handler);
|
|
130
|
+
app.listen(3000);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Access conversation history
|
|
138
|
+
|
|
139
|
+
Give your agent full context by accessing different types of email history.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { CommuneClient } from "commune-ai";
|
|
143
|
+
const client = new CommuneClient({
|
|
144
|
+
apiKey: process.env.COMMUNE_API_KEY,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 1) Thread history - all messages in current conversation
|
|
148
|
+
const threadMessages = await client.messages.listByConversation(
|
|
149
|
+
message.conversation_id,
|
|
150
|
+
{ order: "asc", limit: 20 }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// 2) User history - all emails from/to a specific person
|
|
154
|
+
const userEmails = await client.messages.list({
|
|
155
|
+
sender: "customer@example.com",
|
|
156
|
+
limit: 50,
|
|
157
|
+
order: "desc", // newest first
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// 3) Inbox history - all emails in your domain
|
|
161
|
+
const inboxEmails = await client.messages.list({
|
|
162
|
+
inbox_id: "inbox_123",
|
|
163
|
+
limit: 100,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 4) Organization search - semantic search across all emails (coming soon)
|
|
167
|
+
const searchResults = await client.search({
|
|
168
|
+
query: "pricing questions from last week",
|
|
169
|
+
limit: 10,
|
|
170
|
+
threshold: 0.8,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
console.log(`Found ${searchResults.length} relevant emails`);
|
|
174
|
+
for (const result of searchResults) {
|
|
175
|
+
console.log(`Match (${result.similarity.toFixed(2)}): ${result.message.content}`);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Complete agent example
|
|
182
|
+
|
|
183
|
+
A production-ready agent that receives emails, accesses conversation history, and provides contextual responses.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import "dotenv/config";
|
|
187
|
+
import express from "express";
|
|
188
|
+
import { CommuneClient, createWebhookHandler } from "commune-ai";
|
|
189
|
+
|
|
190
|
+
const client = new CommuneClient({
|
|
191
|
+
apiKey: process.env.COMMUNE_API_KEY,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const handler = createWebhookHandler({
|
|
195
|
+
onEvent: async (message, context) => {
|
|
196
|
+
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
197
|
+
if (!sender) return;
|
|
198
|
+
|
|
199
|
+
// Get conversation history for context
|
|
200
|
+
const threadHistory = await client.messages.listByConversation(
|
|
201
|
+
message.conversation_id,
|
|
202
|
+
{ order: "asc", limit: 10 }
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Get user's full email history
|
|
206
|
+
const userHistory = await client.messages.list({
|
|
207
|
+
sender,
|
|
208
|
+
limit: 20,
|
|
209
|
+
order: "desc",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Analyze the conversation
|
|
213
|
+
const isFollowUp = threadHistory.length > 1;
|
|
214
|
+
const userEmailCount = userHistory.length;
|
|
215
|
+
|
|
216
|
+
// Generate contextual response
|
|
217
|
+
let response = "";
|
|
218
|
+
if (isFollowUp) {
|
|
219
|
+
response = `Welcome back! I see this is a follow-up to our conversation. `;
|
|
220
|
+
} else if (userEmailCount > 1) {
|
|
221
|
+
response = `I see you've emailed us before. `;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
response += `Regarding "${message.content.substring(0, 50)}..." - I'll help you with that.`;
|
|
225
|
+
|
|
226
|
+
// Send contextual reply
|
|
227
|
+
await client.messages.send({
|
|
228
|
+
to: sender,
|
|
229
|
+
text: response,
|
|
230
|
+
conversation_id: message.conversation_id,
|
|
231
|
+
subject: `Re: ${message.metadata.subject}`,
|
|
232
|
+
domainId: context.payload.domainId,
|
|
233
|
+
inboxId: context.payload.inboxId,
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const app = express();
|
|
239
|
+
app.post("/webhook", express.raw({ type: "*/*" }), handler);
|
|
240
|
+
|
|
241
|
+
// Health check endpoint
|
|
242
|
+
app.get("/health", (req, res) => res.json({ status: "ok" }));
|
|
243
|
+
|
|
244
|
+
app.listen(3000, () => console.log("Agent running on port 3000"));
|
|
245
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AttachmentRecord, ConversationListParams, CreateDomainPayload, DomainEntry, InboxEntry, MessageListParams, SemanticSearchParams, SemanticSearchResult, SendMessagePayload, UnifiedMessage } from './types.js';
|
|
2
|
+
export type ClientOptions = {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
fetcher?: typeof fetch;
|
|
7
|
+
};
|
|
8
|
+
export declare class CommuneClient {
|
|
9
|
+
private baseUrl;
|
|
10
|
+
private apiKey;
|
|
11
|
+
private headers?;
|
|
12
|
+
private fetcher;
|
|
13
|
+
constructor(options: ClientOptions);
|
|
14
|
+
private request;
|
|
15
|
+
domains: {
|
|
16
|
+
list: () => Promise<DomainEntry[]>;
|
|
17
|
+
create: (payload: CreateDomainPayload) => Promise<Record<string, unknown>>;
|
|
18
|
+
get: (domainId: string) => Promise<Record<string, unknown>>;
|
|
19
|
+
verify: (domainId: string) => Promise<Record<string, unknown>>;
|
|
20
|
+
records: (domainId: string) => Promise<unknown[]>;
|
|
21
|
+
status: (domainId: string) => Promise<Record<string, unknown>>;
|
|
22
|
+
createWebhook: (domainId: string, payload?: {
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
events?: string[];
|
|
25
|
+
}) => Promise<Record<string, unknown>>;
|
|
26
|
+
saveWebhookSecret: (domainId: string, secret: string) => Promise<Record<string, unknown>>;
|
|
27
|
+
};
|
|
28
|
+
inboxes: {
|
|
29
|
+
list: (domainId: string) => Promise<InboxEntry[]>;
|
|
30
|
+
create: (domainId: string, payload: {
|
|
31
|
+
localPart: string;
|
|
32
|
+
agent?: InboxEntry["agent"];
|
|
33
|
+
webhook?: InboxEntry["webhook"];
|
|
34
|
+
status?: string;
|
|
35
|
+
}) => Promise<InboxEntry>;
|
|
36
|
+
update: (domainId: string, inboxId: string, payload: {
|
|
37
|
+
localPart?: string;
|
|
38
|
+
agent?: InboxEntry["agent"];
|
|
39
|
+
webhook?: InboxEntry["webhook"];
|
|
40
|
+
status?: string;
|
|
41
|
+
}) => Promise<InboxEntry>;
|
|
42
|
+
remove: (domainId: string, inboxId: string) => Promise<{
|
|
43
|
+
ok: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
setWebhook: (domainId: string, inboxId: string, payload: {
|
|
46
|
+
endpoint: string;
|
|
47
|
+
events?: string[];
|
|
48
|
+
}) => Promise<InboxEntry>;
|
|
49
|
+
};
|
|
50
|
+
messages: {
|
|
51
|
+
send: (payload: SendMessagePayload) => Promise<Record<string, unknown>>;
|
|
52
|
+
list: (params: MessageListParams) => Promise<UnifiedMessage[]>;
|
|
53
|
+
listByConversation: (conversationId: string, params?: ConversationListParams) => Promise<UnifiedMessage[]>;
|
|
54
|
+
};
|
|
55
|
+
search: (params: SemanticSearchParams) => Promise<SemanticSearchResult[]>;
|
|
56
|
+
attachments: {
|
|
57
|
+
get: (attachmentId: string) => Promise<AttachmentRecord>;
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://web-production-3f46f.up.railway.app';
|
|
2
|
+
const buildQuery = (params) => {
|
|
3
|
+
const query = new URLSearchParams();
|
|
4
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
5
|
+
if (value === undefined || value === null || value === '') {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
query.set(key, String(value));
|
|
9
|
+
});
|
|
10
|
+
const serialized = query.toString();
|
|
11
|
+
return serialized ? `?${serialized}` : '';
|
|
12
|
+
};
|
|
13
|
+
export class CommuneClient {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.domains = {
|
|
16
|
+
list: async () => {
|
|
17
|
+
const response = await this.request(`/api/domains`);
|
|
18
|
+
if (Array.isArray(response)) {
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
21
|
+
return Array.isArray(response.data) ? response.data : [];
|
|
22
|
+
},
|
|
23
|
+
create: async (payload) => {
|
|
24
|
+
return this.request(`/api/domains`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
json: payload,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
get: async (domainId) => {
|
|
30
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}`);
|
|
31
|
+
},
|
|
32
|
+
verify: async (domainId) => {
|
|
33
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/verify`, { method: 'POST' });
|
|
34
|
+
},
|
|
35
|
+
records: async (domainId) => {
|
|
36
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/records`);
|
|
37
|
+
},
|
|
38
|
+
status: async (domainId) => {
|
|
39
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/status`);
|
|
40
|
+
},
|
|
41
|
+
createWebhook: async (domainId, payload) => {
|
|
42
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/webhook`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
json: payload,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
saveWebhookSecret: async (domainId, secret) => {
|
|
48
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/webhook/secret`, { method: 'POST', json: { secret } });
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
this.inboxes = {
|
|
52
|
+
list: async (domainId) => {
|
|
53
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes`);
|
|
54
|
+
},
|
|
55
|
+
create: async (domainId, payload) => {
|
|
56
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
json: payload,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
update: async (domainId, inboxId, payload) => {
|
|
62
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}`, {
|
|
63
|
+
method: 'PUT',
|
|
64
|
+
json: payload,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
remove: async (domainId, inboxId) => {
|
|
68
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}`, { method: 'DELETE' });
|
|
69
|
+
},
|
|
70
|
+
setWebhook: async (domainId, inboxId, payload) => {
|
|
71
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/webhook`, { method: 'POST', json: payload });
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
this.messages = {
|
|
75
|
+
send: async (payload) => {
|
|
76
|
+
// Always send as email (Slack support coming soon)
|
|
77
|
+
const emailPayload = { ...payload, channel: 'email' };
|
|
78
|
+
return this.request('/api/messages/send', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
json: emailPayload,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
list: async (params) => {
|
|
84
|
+
return this.request(`/api/messages${buildQuery({
|
|
85
|
+
sender: params.sender,
|
|
86
|
+
channel: params.channel,
|
|
87
|
+
before: params.before,
|
|
88
|
+
after: params.after,
|
|
89
|
+
limit: params.limit,
|
|
90
|
+
order: params.order,
|
|
91
|
+
domain_id: params.domain_id,
|
|
92
|
+
inbox_id: params.inbox_id,
|
|
93
|
+
})}`);
|
|
94
|
+
},
|
|
95
|
+
listByConversation: async (conversationId, params = {}) => {
|
|
96
|
+
return this.request(`/api/conversations/${encodeURIComponent(conversationId)}/messages${buildQuery({
|
|
97
|
+
limit: params.limit,
|
|
98
|
+
order: params.order,
|
|
99
|
+
})}`);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
// Semantic search across all organizational emails (coming soon)
|
|
103
|
+
this.search = async (params) => {
|
|
104
|
+
// Placeholder for future semantic search implementation
|
|
105
|
+
// This will search across all emails in the organization
|
|
106
|
+
return this.request(`/api/search${buildQuery({
|
|
107
|
+
q: params.query,
|
|
108
|
+
limit: params.limit || 10,
|
|
109
|
+
threshold: params.threshold || 0.7,
|
|
110
|
+
before: params.before,
|
|
111
|
+
after: params.after,
|
|
112
|
+
sender: params.sender,
|
|
113
|
+
})}`);
|
|
114
|
+
};
|
|
115
|
+
this.attachments = {
|
|
116
|
+
get: async (attachmentId) => {
|
|
117
|
+
return this.request(`/api/attachments/${encodeURIComponent(attachmentId)}`);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
if (!options.apiKey) {
|
|
121
|
+
throw new Error('API key is required. Get one from your Commune dashboard at https://commune.ai');
|
|
122
|
+
}
|
|
123
|
+
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
124
|
+
this.apiKey = options.apiKey;
|
|
125
|
+
this.headers = options.headers;
|
|
126
|
+
this.fetcher = options.fetcher || fetch;
|
|
127
|
+
}
|
|
128
|
+
async request(path, options = {}) {
|
|
129
|
+
const { json, headers, ...rest } = options;
|
|
130
|
+
const response = await this.fetcher(`${this.baseUrl}${path}`, {
|
|
131
|
+
...rest,
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
|
|
135
|
+
...(this.headers || {}),
|
|
136
|
+
...(headers || {}),
|
|
137
|
+
},
|
|
138
|
+
body: json ? JSON.stringify(json) : rest.body,
|
|
139
|
+
});
|
|
140
|
+
const data = (await response.json().catch(() => ({})));
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorValue = data?.error;
|
|
143
|
+
const message = errorValue?.message ||
|
|
144
|
+
(typeof data?.error === 'string' ? data.error : undefined) ||
|
|
145
|
+
response.statusText;
|
|
146
|
+
throw new Error(message);
|
|
147
|
+
}
|
|
148
|
+
if (data.data !== undefined) {
|
|
149
|
+
return data.data;
|
|
150
|
+
}
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CommuneClient } from './client.js';
|
|
2
|
+
export type { ApiError, ApiResponse, AttachmentRecord, Channel, ConversationListParams, CreateDomainPayload, Direction, DomainEntry, DomainWebhook, InboxEntry, InboxWebhook, InboundEmailWebhookPayload, MessageListParams, MessageMetadata, Participant, ParticipantRole, SemanticSearchParams, SemanticSearchResult, SendMessagePayload, SvixHeaders, UnifiedMessage, } from './types.js';
|
|
3
|
+
export { verifyResendWebhook } from './webhooks.js';
|
|
4
|
+
export { createWebhookHandler } from './listener.js';
|
|
5
|
+
export type { CommuneWebhookEvent, CommuneWebhookHandlerContext, CreateWebhookHandlerOptions, } from './listener.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InboundEmailWebhookPayload, UnifiedMessage } from './types.js';
|
|
2
|
+
export type CommuneWebhookEvent = InboundEmailWebhookPayload;
|
|
3
|
+
export type CommuneWebhookHandlerContext = {
|
|
4
|
+
payload: CommuneWebhookEvent;
|
|
5
|
+
rawBody: string;
|
|
6
|
+
headers: Record<string, string | undefined>;
|
|
7
|
+
};
|
|
8
|
+
export type CreateWebhookHandlerOptions = {
|
|
9
|
+
onEvent: (message: UnifiedMessage, context: CommuneWebhookHandlerContext) => Promise<void> | void;
|
|
10
|
+
verify?: (input: {
|
|
11
|
+
rawBody: string;
|
|
12
|
+
headers: Record<string, string | undefined>;
|
|
13
|
+
}) => boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare const createWebhookHandler: ({ onEvent, verify }: CreateWebhookHandlerOptions) => (req: any, res?: any) => Promise<Response | undefined>;
|
package/dist/listener.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const collectNodeBody = async (req) => {
|
|
2
|
+
if (req?.body) {
|
|
3
|
+
if (Buffer.isBuffer(req.body)) {
|
|
4
|
+
return req.body.toString('utf8');
|
|
5
|
+
}
|
|
6
|
+
if (typeof req.body === 'string') {
|
|
7
|
+
return req.body;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return await new Promise((resolve, reject) => {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
13
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
14
|
+
req.on('error', reject);
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
const getHeadersFromNode = (req) => {
|
|
18
|
+
const headers = {};
|
|
19
|
+
if (!req?.headers) {
|
|
20
|
+
return headers;
|
|
21
|
+
}
|
|
22
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
23
|
+
headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
|
|
24
|
+
}
|
|
25
|
+
return headers;
|
|
26
|
+
};
|
|
27
|
+
const parsePayload = (rawBody) => {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(rawBody);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export const createWebhookHandler = ({ onEvent, verify }) => {
|
|
36
|
+
return async (req, res) => {
|
|
37
|
+
if (req?.method && req.method !== 'POST') {
|
|
38
|
+
if (res) {
|
|
39
|
+
res.status(405).json({ error: 'Method Not Allowed' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
return new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
|
|
43
|
+
status: 405,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const isWebRequest = typeof req?.text === 'function';
|
|
48
|
+
const rawBody = isWebRequest ? await req.text() : await collectNodeBody(req);
|
|
49
|
+
const headers = isWebRequest
|
|
50
|
+
? {
|
|
51
|
+
'x-commune-signature': req.headers.get('x-commune-signature') || undefined,
|
|
52
|
+
}
|
|
53
|
+
: getHeadersFromNode(req);
|
|
54
|
+
if (verify && !verify({ rawBody, headers })) {
|
|
55
|
+
if (res) {
|
|
56
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
return new Response(JSON.stringify({ error: 'Invalid signature' }), {
|
|
60
|
+
status: 401,
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const payload = parsePayload(rawBody);
|
|
65
|
+
if (!payload) {
|
|
66
|
+
if (res) {
|
|
67
|
+
res.status(400).json({ error: 'Invalid JSON payload' });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
return new Response(JSON.stringify({ error: 'Invalid JSON payload' }), {
|
|
71
|
+
status: 400,
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
await onEvent(payload.message, { payload, rawBody, headers });
|
|
76
|
+
if (res) {
|
|
77
|
+
res.json({ ok: true });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
81
|
+
status: 200,
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export type Channel = 'email';
|
|
2
|
+
export type Direction = 'inbound' | 'outbound';
|
|
3
|
+
export type ParticipantRole = 'sender' | 'to' | 'cc' | 'bcc' | 'mentioned' | 'participant';
|
|
4
|
+
export interface Participant {
|
|
5
|
+
role: ParticipantRole;
|
|
6
|
+
identity: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MessageMetadata {
|
|
9
|
+
created_at: string;
|
|
10
|
+
subject?: string;
|
|
11
|
+
in_reply_to?: string | null;
|
|
12
|
+
references?: string[];
|
|
13
|
+
is_private?: boolean;
|
|
14
|
+
domain_id?: string | null;
|
|
15
|
+
inbox_id?: string | null;
|
|
16
|
+
inbox_address?: string | null;
|
|
17
|
+
message_id?: string | null;
|
|
18
|
+
provider?: 'resend' | 'email' | string;
|
|
19
|
+
raw?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface UnifiedMessage {
|
|
22
|
+
_id?: string;
|
|
23
|
+
channel: Channel;
|
|
24
|
+
message_id: string;
|
|
25
|
+
conversation_id: string;
|
|
26
|
+
direction: Direction;
|
|
27
|
+
participants: Participant[];
|
|
28
|
+
content: string;
|
|
29
|
+
content_html?: string | null;
|
|
30
|
+
attachments: string[];
|
|
31
|
+
created_at: string;
|
|
32
|
+
metadata: MessageMetadata;
|
|
33
|
+
}
|
|
34
|
+
export interface AttachmentRecord {
|
|
35
|
+
attachment_id: string;
|
|
36
|
+
message_id: string;
|
|
37
|
+
filename: string;
|
|
38
|
+
mime_type: string;
|
|
39
|
+
size: number;
|
|
40
|
+
content_base64: string | null;
|
|
41
|
+
source: Channel;
|
|
42
|
+
source_url?: string | null;
|
|
43
|
+
download_error?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface DomainWebhook {
|
|
46
|
+
id?: string;
|
|
47
|
+
endpoint?: string;
|
|
48
|
+
events?: string[];
|
|
49
|
+
secret?: string | null;
|
|
50
|
+
}
|
|
51
|
+
export interface InboxWebhook {
|
|
52
|
+
endpoint?: string;
|
|
53
|
+
events?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface InboxEntry {
|
|
56
|
+
id: string;
|
|
57
|
+
localPart: string;
|
|
58
|
+
address?: string;
|
|
59
|
+
agent?: {
|
|
60
|
+
id?: string;
|
|
61
|
+
name?: string;
|
|
62
|
+
metadata?: Record<string, unknown>;
|
|
63
|
+
};
|
|
64
|
+
webhook?: InboxWebhook;
|
|
65
|
+
createdAt?: string;
|
|
66
|
+
status?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface DomainEntry {
|
|
69
|
+
id: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
status?: string;
|
|
72
|
+
region?: string;
|
|
73
|
+
records?: unknown[];
|
|
74
|
+
createdAt?: string;
|
|
75
|
+
webhook?: DomainWebhook;
|
|
76
|
+
inboxes?: InboxEntry[];
|
|
77
|
+
}
|
|
78
|
+
export interface SendMessagePayload {
|
|
79
|
+
conversation_id?: string;
|
|
80
|
+
to: string | string[];
|
|
81
|
+
text?: string;
|
|
82
|
+
html?: string;
|
|
83
|
+
attachments?: {
|
|
84
|
+
id?: string;
|
|
85
|
+
attachment_id?: string;
|
|
86
|
+
}[];
|
|
87
|
+
subject?: string;
|
|
88
|
+
cc?: string[];
|
|
89
|
+
bcc?: string[];
|
|
90
|
+
headers?: Record<string, string>;
|
|
91
|
+
replyTo?: string | string[];
|
|
92
|
+
reply_to?: string | string[];
|
|
93
|
+
domainId?: string;
|
|
94
|
+
inboxId?: string;
|
|
95
|
+
domain?: string;
|
|
96
|
+
from?: string;
|
|
97
|
+
localPart?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface CreateDomainPayload {
|
|
100
|
+
name: string;
|
|
101
|
+
region?: string;
|
|
102
|
+
capabilities?: {
|
|
103
|
+
sending?: string;
|
|
104
|
+
receiving?: string;
|
|
105
|
+
};
|
|
106
|
+
createWebhook?: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface MessageListParams {
|
|
109
|
+
sender?: string;
|
|
110
|
+
channel?: Channel;
|
|
111
|
+
before?: string;
|
|
112
|
+
after?: string;
|
|
113
|
+
limit?: number;
|
|
114
|
+
order?: 'asc' | 'desc';
|
|
115
|
+
domain_id?: string;
|
|
116
|
+
inbox_id?: string;
|
|
117
|
+
}
|
|
118
|
+
export interface ConversationListParams {
|
|
119
|
+
limit?: number;
|
|
120
|
+
order?: 'asc' | 'desc';
|
|
121
|
+
}
|
|
122
|
+
export interface SvixHeaders {
|
|
123
|
+
id: string;
|
|
124
|
+
timestamp: string;
|
|
125
|
+
signature: string;
|
|
126
|
+
}
|
|
127
|
+
export interface InboundEmailWebhookPayload {
|
|
128
|
+
domainId: string;
|
|
129
|
+
inboxId?: string;
|
|
130
|
+
inboxAddress?: string;
|
|
131
|
+
event: unknown;
|
|
132
|
+
email: unknown;
|
|
133
|
+
message: UnifiedMessage;
|
|
134
|
+
}
|
|
135
|
+
export interface ApiError {
|
|
136
|
+
message?: string;
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
}
|
|
139
|
+
export interface ApiResponse<T> {
|
|
140
|
+
data: T;
|
|
141
|
+
error?: ApiError;
|
|
142
|
+
}
|
|
143
|
+
export interface SemanticSearchParams {
|
|
144
|
+
query: string;
|
|
145
|
+
limit?: number;
|
|
146
|
+
threshold?: number;
|
|
147
|
+
before?: string;
|
|
148
|
+
after?: string;
|
|
149
|
+
sender?: string;
|
|
150
|
+
}
|
|
151
|
+
export interface SemanticSearchResult {
|
|
152
|
+
message: UnifiedMessage;
|
|
153
|
+
similarity: number;
|
|
154
|
+
highlights: string[];
|
|
155
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Webhook } from 'svix';
|
|
2
|
+
export const verifyResendWebhook = (payload, headers, secret) => {
|
|
3
|
+
const webhook = new Webhook(secret);
|
|
4
|
+
const svixHeaders = {
|
|
5
|
+
'svix-id': headers.id,
|
|
6
|
+
'svix-timestamp': headers.timestamp,
|
|
7
|
+
'svix-signature': headers.signature,
|
|
8
|
+
};
|
|
9
|
+
return webhook.verify(payload, svixHeaders);
|
|
10
|
+
};
|
|
11
|
+
// Slack webhook verification (coming soon)
|
|
12
|
+
// export const verifySlackWebhook = ({
|
|
13
|
+
// rawBody,
|
|
14
|
+
// timestamp,
|
|
15
|
+
// signature,
|
|
16
|
+
// signingSecret,
|
|
17
|
+
// }: {
|
|
18
|
+
// rawBody: string;
|
|
19
|
+
// timestamp: string;
|
|
20
|
+
// signature: string;
|
|
21
|
+
// signingSecret: string;
|
|
22
|
+
// }) => {
|
|
23
|
+
// const baseString = `v0:${timestamp}:${rawBody}`;
|
|
24
|
+
// const hmac = crypto
|
|
25
|
+
// .createHmac('sha256', signingSecret)
|
|
26
|
+
// .update(baseString, 'utf8')
|
|
27
|
+
// .digest('hex');
|
|
28
|
+
//
|
|
29
|
+
// const expected = `v0=${hmac}`;
|
|
30
|
+
// const expectedBuffer = Buffer.from(expected, 'utf8');
|
|
31
|
+
// const signatureBuffer = Buffer.from(signature, 'utf8');
|
|
32
|
+
// if (expectedBuffer.length !== signatureBuffer.length) {
|
|
33
|
+
// return false;
|
|
34
|
+
// }
|
|
35
|
+
//
|
|
36
|
+
// return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
|
|
37
|
+
// };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commune-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unified communication SDK for Commune (email + Slack)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"clean": "rm -rf dist"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"svix": "^1.44.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.12.12",
|
|
27
|
+
"typescript": "^5.5.4"
|
|
28
|
+
}
|
|
29
|
+
}
|