commune-ai 0.2.66 → 0.3.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 +133 -54
- package/dist/client.d.ts +34 -3
- package/dist/client.js +65 -26
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +63 -4
- package/dist/webhooks.d.ts +48 -0
- package/dist/webhooks.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,21 @@ Email infrastructure for agents — set up an inbox and send your first email in
|
|
|
6
6
|
npm install commune-ai
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Quickstart](#quickstart-endtoend-in-one-file)
|
|
12
|
+
- [Unified Inbox](#unified-inbox-what-your-webhook-receives)
|
|
13
|
+
- [API Key](#api-key-required)
|
|
14
|
+
- [Attachments](#attachments)
|
|
15
|
+
- [Semantic Search](#semantic-search)
|
|
16
|
+
- [Context](#context-conversation-state)
|
|
17
|
+
- [Email Handling](#email-handling)
|
|
18
|
+
- [Setup Instructions](#setup-instructions-dashboard-first)
|
|
19
|
+
- [Structured Extraction](#structured-extraction-per-inbox)
|
|
20
|
+
- [Webhook Verification](#webhook-verification-commune--your-app)
|
|
21
|
+
- [Full Example](#full-example-single-file)
|
|
22
|
+
- [Security](#security)
|
|
23
|
+
|
|
9
24
|
---
|
|
10
25
|
|
|
11
26
|
## Quickstart (end‑to‑end in one file)
|
|
@@ -34,7 +49,7 @@ const handler = createWebhookHandler({
|
|
|
34
49
|
// Example inbound payload:
|
|
35
50
|
// message = {
|
|
36
51
|
// channel: "email",
|
|
37
|
-
//
|
|
52
|
+
// thread_id: "thread_abc123",
|
|
38
53
|
// participants: [{ role: "sender", identity: "user@example.com" }],
|
|
39
54
|
// content: "Can you help with pricing?"
|
|
40
55
|
// }
|
|
@@ -51,8 +66,7 @@ const handler = createWebhookHandler({
|
|
|
51
66
|
channel: "email",
|
|
52
67
|
to: sender,
|
|
53
68
|
text: agentReply,
|
|
54
|
-
|
|
55
|
-
domainId: context.payload.domainId,
|
|
69
|
+
thread_id: message.thread_id,
|
|
56
70
|
inboxId: context.payload.inboxId,
|
|
57
71
|
});
|
|
58
72
|
},
|
|
@@ -77,7 +91,7 @@ Every inbound email arrives in this shape:
|
|
|
77
91
|
export interface UnifiedMessage {
|
|
78
92
|
channel: "email";
|
|
79
93
|
message_id: string;
|
|
80
|
-
|
|
94
|
+
thread_id: string; // email thread
|
|
81
95
|
participants: { role: string; identity: string }[];
|
|
82
96
|
content: string;
|
|
83
97
|
metadata: { ... };
|
|
@@ -87,7 +101,7 @@ export interface UnifiedMessage {
|
|
|
87
101
|
---
|
|
88
102
|
|
|
89
103
|
## API key (required)
|
|
90
|
-
All `/
|
|
104
|
+
All `/v1/*` requests require an API key. Create one in the dashboard and reuse it in your client.
|
|
91
105
|
|
|
92
106
|
```bash
|
|
93
107
|
export COMMUNE_API_KEY="your_key_from_dashboard"
|
|
@@ -301,48 +315,6 @@ for (const result of withAttachments) {
|
|
|
301
315
|
}
|
|
302
316
|
```
|
|
303
317
|
|
|
304
|
-
## Spam Protection
|
|
305
|
-
Commune includes built-in spam detection and protection to keep your agent safe from malicious emails and prevent abuse.
|
|
306
|
-
|
|
307
|
-
### Automatic Spam Detection
|
|
308
|
-
All incoming emails are automatically analyzed for spam patterns:
|
|
309
|
-
- Content analysis (spam keywords, suspicious patterns)
|
|
310
|
-
- URL validation (broken links, phishing detection)
|
|
311
|
-
- Sender reputation tracking
|
|
312
|
-
- Domain blacklist checking
|
|
313
|
-
|
|
314
|
-
Spam emails are automatically rejected before reaching your webhook, protecting your agent from:
|
|
315
|
-
- Phishing attempts
|
|
316
|
-
- Malicious links
|
|
317
|
-
- Mass spam campaigns
|
|
318
|
-
- Fraudulent content
|
|
319
|
-
|
|
320
|
-
### Outbound Protection
|
|
321
|
-
Commune also protects your sending reputation:
|
|
322
|
-
- Rate limiting per organization tier
|
|
323
|
-
- Content validation for outbound emails
|
|
324
|
-
- Burst detection for mass mailing
|
|
325
|
-
- Automatic blocking of spam-like content
|
|
326
|
-
|
|
327
|
-
This ensures your domain maintains high deliverability and isn't flagged by email providers.
|
|
328
|
-
|
|
329
|
-
### Spam Reporting
|
|
330
|
-
If a spam email gets through, you can report it:
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
import { CommuneClient } from "commune-ai";
|
|
334
|
-
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
335
|
-
|
|
336
|
-
// Report a message as spam
|
|
337
|
-
await client.reportSpam({
|
|
338
|
-
message_id: "msg_123",
|
|
339
|
-
reason: "Unsolicited marketing email",
|
|
340
|
-
classification: "spam" // or "phishing", "malware", "other"
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
The system learns from reports and automatically blocks repeat offenders.
|
|
345
|
-
|
|
346
318
|
## Context (conversation state)
|
|
347
319
|
Commune stores conversation state so your agent can respond with context.
|
|
348
320
|
|
|
@@ -351,7 +323,7 @@ import { CommuneClient } from "commune-ai";
|
|
|
351
323
|
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
352
324
|
|
|
353
325
|
// Thread history (email thread)
|
|
354
|
-
const thread = await client.messages.
|
|
326
|
+
const thread = await client.messages.listByThread(message.thread_id, {
|
|
355
327
|
order: "asc",
|
|
356
328
|
limit: 50,
|
|
357
329
|
});
|
|
@@ -391,8 +363,7 @@ const handler = createWebhookHandler({
|
|
|
391
363
|
channel: "email",
|
|
392
364
|
to: sender,
|
|
393
365
|
text: "Got it — thanks for the message.",
|
|
394
|
-
|
|
395
|
-
domainId: context.payload.domainId,
|
|
366
|
+
thread_id: message.thread_id,
|
|
396
367
|
inboxId: context.payload.inboxId,
|
|
397
368
|
});
|
|
398
369
|
},
|
|
@@ -432,8 +403,7 @@ await client.messages.send({
|
|
|
432
403
|
channel: "email",
|
|
433
404
|
to: "user@example.com",
|
|
434
405
|
text: "Thanks — replying in thread.",
|
|
435
|
-
|
|
436
|
-
domainId: context.payload.domainId,
|
|
406
|
+
thread_id: message.thread_id,
|
|
437
407
|
inboxId: context.payload.inboxId,
|
|
438
408
|
});
|
|
439
409
|
```
|
|
@@ -628,8 +598,7 @@ const handler = createWebhookHandler({
|
|
|
628
598
|
subject: "Your receipt",
|
|
629
599
|
text: "Thanks! Here's your receipt.",
|
|
630
600
|
attachments: [attachment_id],
|
|
631
|
-
|
|
632
|
-
domainId: context.payload.domainId,
|
|
601
|
+
thread_id: message.thread_id,
|
|
633
602
|
inboxId: context.payload.inboxId,
|
|
634
603
|
});
|
|
635
604
|
},
|
|
@@ -639,3 +608,113 @@ const app = express();
|
|
|
639
608
|
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
|
|
640
609
|
app.listen(3000, () => console.log("listening on 3000"));
|
|
641
610
|
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Security
|
|
615
|
+
|
|
616
|
+
Commune is built as production email infrastructure — deliverability, authentication, and abuse prevention are handled at the platform level so you don't have to build them yourself.
|
|
617
|
+
|
|
618
|
+
### Email Authentication (DKIM, SPF, DMARC)
|
|
619
|
+
|
|
620
|
+
Every custom domain you verify through Commune is configured with proper email authentication records:
|
|
621
|
+
|
|
622
|
+
- **DKIM** — All outbound emails are cryptographically signed. The signing keys are managed by Commune; you add the CNAME record to your DNS during domain setup.
|
|
623
|
+
- **SPF** — Sender Policy Framework records authorize Commune's mail servers to send on behalf of your domain, preventing spoofing.
|
|
624
|
+
- **DMARC** — Domain-based Message Authentication is configured to instruct receiving mail servers how to handle unauthenticated messages from your domain.
|
|
625
|
+
|
|
626
|
+
When you verify a domain, the DNS records returned include all three. Once added and verified, your domain passes authentication checks at Gmail, Outlook, and other major providers.
|
|
627
|
+
|
|
628
|
+
### Inbound Spam Protection
|
|
629
|
+
|
|
630
|
+
All inbound email is analyzed before it reaches your inbox or webhook:
|
|
631
|
+
|
|
632
|
+
- **Content analysis** — Subject and body are scored for spam patterns, phishing keywords, and suspicious formatting.
|
|
633
|
+
- **URL validation** — Links are checked for phishing indicators, typosquatting, and low-authority domains.
|
|
634
|
+
- **Sender reputation** — Each sender builds a reputation score over time. Repeat offenders are automatically blocked.
|
|
635
|
+
- **Domain authority** — Sender domains are checked for MX records, SPF, DMARC, valid SSL, and structural red flags.
|
|
636
|
+
- **DNSBL checking** — Sender IPs are checked against DNS-based blackhole lists.
|
|
637
|
+
- **Mass attack detection** — Burst patterns (high volume + low quality) are detected per-organization and throttled automatically.
|
|
638
|
+
|
|
639
|
+
Emails scoring above the reject threshold are silently dropped. Borderline emails are flagged with spam metadata in the message object so your agent can decide how to handle them.
|
|
640
|
+
|
|
641
|
+
### Outbound Protection
|
|
642
|
+
|
|
643
|
+
Outbound emails are validated before sending to protect your domain reputation:
|
|
644
|
+
|
|
645
|
+
- **Content scanning** — Outgoing messages are checked for spam-like patterns before delivery.
|
|
646
|
+
- **Recipient limits** — Maximum 50 recipients per message to prevent mass mailing.
|
|
647
|
+
- **Redis-backed rate limiting** — Distributed sliding-window rate limiting powered by Redis (with in-memory fallback). Accurate across multiple server instances.
|
|
648
|
+
- **Burst detection** — Real-time burst detection using Redis sorted sets with dual sliding windows (10-second and 60-second). Sudden spikes in send volume are automatically throttled with a `429` response.
|
|
649
|
+
|
|
650
|
+
### Attachment Scanning
|
|
651
|
+
|
|
652
|
+
All inbound attachments are scanned before storage:
|
|
653
|
+
|
|
654
|
+
- **ClamAV integration** — When a ClamAV daemon is available (via `CLAMAV_HOST`), attachments are scanned using the INSTREAM protocol over TCP.
|
|
655
|
+
- **Heuristic fallback** — When ClamAV is unavailable, a multi-layer heuristic scanner checks file extensions, MIME types, magic bytes, double extensions, VBA macros in Office documents, and suspicious archive files.
|
|
656
|
+
- **Known threat database** — File hashes (SHA-256) are stored for all detected threats. Subsequent uploads of the same file are instantly blocked.
|
|
657
|
+
- **Quarantine** — Dangerous attachments are quarantined (not stored) and flagged in the message metadata.
|
|
658
|
+
|
|
659
|
+
### Encryption at Rest
|
|
660
|
+
|
|
661
|
+
When `EMAIL_ENCRYPTION_KEY` is set (64 hex characters = 256 bits):
|
|
662
|
+
|
|
663
|
+
- Email body (`content`, `content_html`) and subject are encrypted with **AES-256-GCM** before storage in MongoDB.
|
|
664
|
+
- Attachment content stored in the database is also encrypted.
|
|
665
|
+
- Each encrypted value uses a unique random IV and includes a GCM authentication tag for tamper detection.
|
|
666
|
+
- Decryption is transparent — the API returns plaintext to authorized callers.
|
|
667
|
+
- Existing unencrypted data continues to work (the system detects the `enc:` prefix).
|
|
668
|
+
|
|
669
|
+
### DMARC Reporting
|
|
670
|
+
|
|
671
|
+
Commune provides end-to-end DMARC aggregate report processing:
|
|
672
|
+
|
|
673
|
+
- **Report ingestion** — Submit DMARC XML reports via `POST /v1/dmarc/reports` (supports XML, gzip, and zip formats).
|
|
674
|
+
- **Automatic parsing** — Reports are parsed following RFC 7489 Appendix C, extracting per-record authentication results.
|
|
675
|
+
- **Failure alerting** — Authentication failures above 10% trigger warnings in server logs.
|
|
676
|
+
- **Summary API** — `GET /v1/dmarc/summary?domain=example.com&days=30` returns pass/fail rates, DKIM/SPF breakdowns, and top sending IPs.
|
|
677
|
+
- **Auto-cleanup** — Reports older than 1 year are automatically removed via TTL index.
|
|
678
|
+
|
|
679
|
+
### Delivery Metrics & Bounce Handling
|
|
680
|
+
|
|
681
|
+
Bounces, complaints, and delivery events are tracked automatically:
|
|
682
|
+
|
|
683
|
+
- **Automatic suppression** — Hard bounces and spam complaints automatically add recipients to the suppression list.
|
|
684
|
+
- **Delivery metrics API** — `GET /v1/delivery/metrics?inbox_id=...&days=7` returns sent, delivered, bounced, complained, and failed counts with calculated rates.
|
|
685
|
+
- **Event stream** — `GET /v1/delivery/events?inbox_id=...` lists recent delivery events for debugging.
|
|
686
|
+
- **Suppression list** — `GET /v1/delivery/suppressions?inbox_id=...` shows all suppressed addresses.
|
|
687
|
+
|
|
688
|
+
### Rate Limits
|
|
689
|
+
|
|
690
|
+
| Tier | Emails/hour | Emails/day | Domains/day | Inboxes/day |
|
|
691
|
+
|------|-------------|------------|-------------|-------------|
|
|
692
|
+
| Free | 100 | 1,000 | 5 | 50 |
|
|
693
|
+
| Pro | 10,000 | 100,000 | 50 | 500 |
|
|
694
|
+
| Enterprise | Unlimited | Unlimited | Unlimited | Unlimited |
|
|
695
|
+
|
|
696
|
+
Rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) are included in API responses.
|
|
697
|
+
|
|
698
|
+
### API Key Security
|
|
699
|
+
|
|
700
|
+
- API keys use the `comm_` prefix followed by 64 cryptographically random hex characters.
|
|
701
|
+
- Keys are **bcrypt-hashed** before storage — the raw key is only shown once at creation.
|
|
702
|
+
- Each key has **granular permission scopes**: `domains:read`, `domains:write`, `inboxes:read`, `inboxes:write`, `threads:read`, `messages:read`, `messages:write`, `attachments:read`, `attachments:write`.
|
|
703
|
+
- Keys are scoped to a single organization and can be revoked or rotated at any time from the dashboard.
|
|
704
|
+
- Maximum 10 active keys per organization.
|
|
705
|
+
|
|
706
|
+
### Webhook Verification
|
|
707
|
+
|
|
708
|
+
Inbound webhook payloads from Commune are signed with your inbox webhook secret. Always verify the signature before processing — see the [Webhook Verification](#webhook-verification-commune--your-app) section above for the full implementation.
|
|
709
|
+
|
|
710
|
+
### Attachment Security
|
|
711
|
+
|
|
712
|
+
- Uploaded attachments are stored in secure cloud storage with per-object access control.
|
|
713
|
+
- Download URLs are **temporary** (default 1 hour, configurable up to 24 hours) and expire automatically.
|
|
714
|
+
- Attachments are scoped to the organization that uploaded them.
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## License
|
|
719
|
+
|
|
720
|
+
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -22,9 +22,37 @@ export declare class CommuneClient {
|
|
|
22
22
|
status: (domainId: string) => Promise<Record<string, unknown>>;
|
|
23
23
|
};
|
|
24
24
|
inboxes: {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* List inboxes.
|
|
27
|
+
*
|
|
28
|
+
* @param domainId - Optional domain ID to filter by. If omitted, lists all inboxes across all domains.
|
|
29
|
+
* @example
|
|
30
|
+
* // List all inboxes
|
|
31
|
+
* const allInboxes = await commune.inboxes.list();
|
|
32
|
+
*
|
|
33
|
+
* // List inboxes for a specific domain
|
|
34
|
+
* const domainInboxes = await commune.inboxes.list('domain-id');
|
|
35
|
+
*/
|
|
36
|
+
list: (domainId?: string) => Promise<InboxEntry[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Create a new inbox.
|
|
39
|
+
*
|
|
40
|
+
* @param payload - Inbox configuration
|
|
41
|
+
* @example
|
|
42
|
+
* // Simple creation with auto-resolved domain (recommended)
|
|
43
|
+
* const inbox = await commune.inboxes.create({
|
|
44
|
+
* localPart: 'support',
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // With explicit domain (for custom domains)
|
|
48
|
+
* const inbox = await commune.inboxes.create({
|
|
49
|
+
* localPart: 'support',
|
|
50
|
+
* domainId: 'my-domain-id',
|
|
51
|
+
* });
|
|
52
|
+
*/
|
|
53
|
+
create: (payload: {
|
|
27
54
|
localPart: string;
|
|
55
|
+
domainId?: string;
|
|
28
56
|
agent?: InboxEntry["agent"];
|
|
29
57
|
webhook?: InboxEntry["webhook"];
|
|
30
58
|
status?: string;
|
|
@@ -60,7 +88,10 @@ export declare class CommuneClient {
|
|
|
60
88
|
messages: {
|
|
61
89
|
send: (payload: SendMessagePayload) => Promise<Record<string, unknown>>;
|
|
62
90
|
list: (params: MessageListParams) => Promise<UnifiedMessage[]>;
|
|
63
|
-
|
|
91
|
+
listByThread: (threadId: string, params?: {
|
|
92
|
+
limit?: number;
|
|
93
|
+
order?: "asc" | "desc";
|
|
94
|
+
}) => Promise<UnifiedMessage[]>;
|
|
64
95
|
};
|
|
65
96
|
conversations: {
|
|
66
97
|
search: (query: string, filter: SearchFilter, options?: SearchOptions) => Promise<SearchResult[]>;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SearchClient } from './client/search.js';
|
|
2
|
-
const DEFAULT_BASE_URL = 'https://
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.commune.email';
|
|
3
3
|
const buildQuery = (params) => {
|
|
4
4
|
const query = new URLSearchParams();
|
|
5
5
|
Object.entries(params).forEach(([key, value]) => {
|
|
@@ -15,71 +15,110 @@ export class CommuneClient {
|
|
|
15
15
|
constructor(options) {
|
|
16
16
|
this.domains = {
|
|
17
17
|
list: async () => {
|
|
18
|
-
const response = await this.request(`/
|
|
18
|
+
const response = await this.request(`/v1/domains`);
|
|
19
19
|
if (Array.isArray(response)) {
|
|
20
20
|
return response;
|
|
21
21
|
}
|
|
22
22
|
return Array.isArray(response.data) ? response.data : [];
|
|
23
23
|
},
|
|
24
24
|
create: async (payload) => {
|
|
25
|
-
return this.request(`/
|
|
25
|
+
return this.request(`/v1/domains`, {
|
|
26
26
|
method: 'POST',
|
|
27
27
|
json: payload,
|
|
28
28
|
});
|
|
29
29
|
},
|
|
30
30
|
get: async (domainId) => {
|
|
31
|
-
return this.request(`/
|
|
31
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}`);
|
|
32
32
|
},
|
|
33
33
|
verify: async (domainId) => {
|
|
34
|
-
return this.request(`/
|
|
34
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/verify`, { method: 'POST' });
|
|
35
35
|
},
|
|
36
36
|
records: async (domainId) => {
|
|
37
|
-
return this.request(`/
|
|
37
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/records`);
|
|
38
38
|
},
|
|
39
39
|
status: async (domainId) => {
|
|
40
|
-
return this.request(`/
|
|
40
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/status`);
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
this.inboxes = {
|
|
44
|
+
/**
|
|
45
|
+
* List inboxes.
|
|
46
|
+
*
|
|
47
|
+
* @param domainId - Optional domain ID to filter by. If omitted, lists all inboxes across all domains.
|
|
48
|
+
* @example
|
|
49
|
+
* // List all inboxes
|
|
50
|
+
* const allInboxes = await commune.inboxes.list();
|
|
51
|
+
*
|
|
52
|
+
* // List inboxes for a specific domain
|
|
53
|
+
* const domainInboxes = await commune.inboxes.list('domain-id');
|
|
54
|
+
*/
|
|
44
55
|
list: async (domainId) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return this.request(`/
|
|
56
|
+
if (domainId) {
|
|
57
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes`);
|
|
58
|
+
}
|
|
59
|
+
return this.request(`/v1/inboxes`);
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Create a new inbox.
|
|
63
|
+
*
|
|
64
|
+
* @param payload - Inbox configuration
|
|
65
|
+
* @example
|
|
66
|
+
* // Simple creation with auto-resolved domain (recommended)
|
|
67
|
+
* const inbox = await commune.inboxes.create({
|
|
68
|
+
* localPart: 'support',
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // With explicit domain (for custom domains)
|
|
72
|
+
* const inbox = await commune.inboxes.create({
|
|
73
|
+
* localPart: 'support',
|
|
74
|
+
* domainId: 'my-domain-id',
|
|
75
|
+
* });
|
|
76
|
+
*/
|
|
77
|
+
create: async (payload) => {
|
|
78
|
+
const { domainId, ...rest } = payload;
|
|
79
|
+
// If domainId provided, use domain-scoped endpoint for explicit control
|
|
80
|
+
if (domainId) {
|
|
81
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
json: rest,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Otherwise use simple auto-resolved endpoint
|
|
87
|
+
return this.request(`/v1/inboxes`, {
|
|
49
88
|
method: 'POST',
|
|
50
89
|
json: payload,
|
|
51
90
|
});
|
|
52
91
|
},
|
|
53
92
|
update: async (domainId, inboxId, payload) => {
|
|
54
|
-
return this.request(`/
|
|
93
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}`, {
|
|
55
94
|
method: 'PUT',
|
|
56
95
|
json: payload,
|
|
57
96
|
});
|
|
58
97
|
},
|
|
59
98
|
remove: async (domainId, inboxId) => {
|
|
60
|
-
return this.request(`/
|
|
99
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}`, { method: 'DELETE' });
|
|
61
100
|
},
|
|
62
101
|
setWebhook: async (domainId, inboxId, payload) => {
|
|
63
|
-
return this.request(`/
|
|
102
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/webhook`, { method: 'POST', json: payload });
|
|
64
103
|
},
|
|
65
104
|
setExtractionSchema: async (payload) => {
|
|
66
105
|
const { domainId, inboxId, schema } = payload;
|
|
67
|
-
return this.request(`/
|
|
106
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/extraction-schema`, { method: 'PUT', json: schema });
|
|
68
107
|
},
|
|
69
108
|
removeExtractionSchema: async (payload) => {
|
|
70
109
|
const { domainId, inboxId } = payload;
|
|
71
|
-
return this.request(`/
|
|
110
|
+
return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/extraction-schema`, { method: 'DELETE' });
|
|
72
111
|
},
|
|
73
112
|
};
|
|
74
113
|
this.messages = {
|
|
75
114
|
send: async (payload) => {
|
|
76
|
-
return this.request('/
|
|
115
|
+
return this.request('/v1/messages/send', {
|
|
77
116
|
method: 'POST',
|
|
78
117
|
json: payload,
|
|
79
118
|
});
|
|
80
119
|
},
|
|
81
120
|
list: async (params) => {
|
|
82
|
-
return this.request(`/
|
|
121
|
+
return this.request(`/v1/messages${buildQuery({
|
|
83
122
|
sender: params.sender,
|
|
84
123
|
channel: params.channel,
|
|
85
124
|
before: params.before,
|
|
@@ -90,8 +129,8 @@ export class CommuneClient {
|
|
|
90
129
|
inbox_id: params.inbox_id,
|
|
91
130
|
})}`);
|
|
92
131
|
},
|
|
93
|
-
|
|
94
|
-
return this.request(`/
|
|
132
|
+
listByThread: async (threadId, params) => {
|
|
133
|
+
return this.request(`/v1/threads/${encodeURIComponent(threadId)}/messages${buildQuery({
|
|
95
134
|
limit: params?.limit,
|
|
96
135
|
order: params?.order,
|
|
97
136
|
})}`);
|
|
@@ -99,7 +138,7 @@ export class CommuneClient {
|
|
|
99
138
|
};
|
|
100
139
|
this.conversations = {
|
|
101
140
|
search: async (query, filter, options) => {
|
|
102
|
-
return this.request('/
|
|
141
|
+
return this.request('/v1/search', {
|
|
103
142
|
method: 'POST',
|
|
104
143
|
json: {
|
|
105
144
|
query,
|
|
@@ -109,7 +148,7 @@ export class CommuneClient {
|
|
|
109
148
|
});
|
|
110
149
|
},
|
|
111
150
|
index: async (organizationId, conversation) => {
|
|
112
|
-
return this.request('/
|
|
151
|
+
return this.request('/v1/search/index', {
|
|
113
152
|
method: 'POST',
|
|
114
153
|
json: {
|
|
115
154
|
organizationId,
|
|
@@ -118,7 +157,7 @@ export class CommuneClient {
|
|
|
118
157
|
});
|
|
119
158
|
},
|
|
120
159
|
indexBatch: async (organizationId, conversations) => {
|
|
121
|
-
return this.request('/
|
|
160
|
+
return this.request('/v1/search/index/batch', {
|
|
122
161
|
method: 'POST',
|
|
123
162
|
json: {
|
|
124
163
|
organizationId,
|
|
@@ -157,7 +196,7 @@ export class CommuneClient {
|
|
|
157
196
|
};
|
|
158
197
|
this.attachments = {
|
|
159
198
|
upload: async (content, filename, mimeType) => {
|
|
160
|
-
return this.request('/
|
|
199
|
+
return this.request('/v1/attachments/upload', {
|
|
161
200
|
method: 'POST',
|
|
162
201
|
body: JSON.stringify({ content, filename, mimeType }),
|
|
163
202
|
});
|
|
@@ -165,9 +204,9 @@ export class CommuneClient {
|
|
|
165
204
|
get: async (attachmentId, options) => {
|
|
166
205
|
if (options?.url) {
|
|
167
206
|
const expiresIn = options.expiresIn || 3600;
|
|
168
|
-
return this.request(`/
|
|
207
|
+
return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}/url?expires_in=${expiresIn}`);
|
|
169
208
|
}
|
|
170
|
-
return this.request(`/
|
|
209
|
+
return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}`);
|
|
171
210
|
},
|
|
172
211
|
};
|
|
173
212
|
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
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, SendMessagePayload, SvixHeaders, Thread, ThreadListParams, ThreadListResponse, UnifiedMessage, } from './types.js';
|
|
3
|
-
export { verifyResendWebhook } from './webhooks.js';
|
|
2
|
+
export type { ApiError, ApiResponse, AttachmentRecord, Channel, ConversationListParams, CreateDomainPayload, CreateInboxPayload, Direction, DomainEntry, DomainWebhook, InboxEntry, InboxWebhook, InboundEmailWebhookPayload, MessageListParams, MessageMetadata, Participant, ParticipantRole, SendMessagePayload, SvixHeaders, Thread, ThreadListParams, ThreadListResponse, UnifiedMessage, } from './types.js';
|
|
3
|
+
export { verifyResendWebhook, verifyCommuneWebhook, computeCommuneSignature } from './webhooks.js';
|
|
4
|
+
export type { CommuneWebhookHeaders } from './webhooks.js';
|
|
4
5
|
export { createWebhookHandler } from './listener.js';
|
|
5
6
|
export type { CommuneWebhookEvent, CommuneWebhookHandlerContext, CreateWebhookHandlerOptions, } from './listener.js';
|
package/dist/index.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -18,12 +18,21 @@ export interface MessageMetadata {
|
|
|
18
18
|
provider?: 'resend' | 'email' | string;
|
|
19
19
|
raw?: unknown;
|
|
20
20
|
extracted_data?: Record<string, any>;
|
|
21
|
+
spam_score?: number | null;
|
|
22
|
+
spam_action?: 'accept' | 'flag' | 'reject' | string | null;
|
|
23
|
+
spam_flagged?: boolean | null;
|
|
24
|
+
delivery_status?: 'sent' | 'delivered' | 'bounced' | 'failed' | 'complained' | null;
|
|
25
|
+
prompt_injection_checked?: boolean;
|
|
26
|
+
prompt_injection_detected?: boolean;
|
|
27
|
+
prompt_injection_risk?: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
28
|
+
prompt_injection_score?: number;
|
|
29
|
+
prompt_injection_signals?: string;
|
|
21
30
|
}
|
|
22
31
|
export interface UnifiedMessage {
|
|
23
32
|
_id?: string;
|
|
24
33
|
channel: Channel;
|
|
25
34
|
message_id: string;
|
|
26
|
-
|
|
35
|
+
thread_id: string;
|
|
27
36
|
direction: Direction;
|
|
28
37
|
participants: Participant[];
|
|
29
38
|
content: string;
|
|
@@ -81,6 +90,7 @@ export interface InboxEntry {
|
|
|
81
90
|
id: string;
|
|
82
91
|
localPart: string;
|
|
83
92
|
address?: string;
|
|
93
|
+
displayName?: string;
|
|
84
94
|
agent?: {
|
|
85
95
|
id?: string;
|
|
86
96
|
name?: string;
|
|
@@ -96,6 +106,38 @@ export interface InboxEntry {
|
|
|
96
106
|
createdAt?: string;
|
|
97
107
|
status?: string;
|
|
98
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Payload for creating a new inbox.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Simple creation with auto-resolved domain
|
|
114
|
+
* const payload: CreateInboxPayload = {
|
|
115
|
+
* localPart: 'support',
|
|
116
|
+
* };
|
|
117
|
+
*
|
|
118
|
+
* // With explicit domain
|
|
119
|
+
* const payload: CreateInboxPayload = {
|
|
120
|
+
* localPart: 'support',
|
|
121
|
+
* domainId: 'domain-id',
|
|
122
|
+
* };
|
|
123
|
+
*/
|
|
124
|
+
export interface CreateInboxPayload {
|
|
125
|
+
/** The part before @ in the email address (e.g., "support" → support@domain.com) */
|
|
126
|
+
localPart: string;
|
|
127
|
+
/** Optional domain ID. If omitted, Commune auto-assigns to an available domain. */
|
|
128
|
+
domainId?: string;
|
|
129
|
+
/** Optional agent configuration */
|
|
130
|
+
agent?: {
|
|
131
|
+
name?: string;
|
|
132
|
+
metadata?: Record<string, unknown>;
|
|
133
|
+
};
|
|
134
|
+
/** Optional webhook configuration */
|
|
135
|
+
webhook?: InboxWebhook;
|
|
136
|
+
/** Optional status */
|
|
137
|
+
status?: string;
|
|
138
|
+
/** Optional display name shown in email clients */
|
|
139
|
+
displayName?: string;
|
|
140
|
+
}
|
|
99
141
|
export interface DomainEntry {
|
|
100
142
|
id: string;
|
|
101
143
|
name?: string;
|
|
@@ -107,7 +149,7 @@ export interface DomainEntry {
|
|
|
107
149
|
inboxes?: InboxEntry[];
|
|
108
150
|
}
|
|
109
151
|
export interface SendMessagePayload {
|
|
110
|
-
|
|
152
|
+
thread_id?: string;
|
|
111
153
|
to: string | string[];
|
|
112
154
|
text?: string;
|
|
113
155
|
html?: string;
|
|
@@ -143,6 +185,7 @@ export interface MessageListParams {
|
|
|
143
185
|
domain_id?: string;
|
|
144
186
|
inbox_id?: string;
|
|
145
187
|
}
|
|
188
|
+
/** @deprecated Use ThreadListParams instead */
|
|
146
189
|
export interface ConversationListParams {
|
|
147
190
|
limit?: number;
|
|
148
191
|
order?: 'asc' | 'desc';
|
|
@@ -154,6 +197,21 @@ export interface SvixHeaders {
|
|
|
154
197
|
timestamp: string;
|
|
155
198
|
signature: string;
|
|
156
199
|
}
|
|
200
|
+
export interface WebhookSecurityContext {
|
|
201
|
+
spam: {
|
|
202
|
+
checked: boolean;
|
|
203
|
+
score: number;
|
|
204
|
+
action: string;
|
|
205
|
+
flagged: boolean;
|
|
206
|
+
};
|
|
207
|
+
prompt_injection: {
|
|
208
|
+
checked: boolean;
|
|
209
|
+
detected: boolean;
|
|
210
|
+
risk_level: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
211
|
+
confidence: number;
|
|
212
|
+
summary?: string;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
157
215
|
export interface InboundEmailWebhookPayload {
|
|
158
216
|
domainId: string;
|
|
159
217
|
inboxId?: string;
|
|
@@ -163,6 +221,7 @@ export interface InboundEmailWebhookPayload {
|
|
|
163
221
|
message: UnifiedMessage;
|
|
164
222
|
extractedData?: Record<string, any>;
|
|
165
223
|
attachments?: AttachmentMetadata[];
|
|
224
|
+
security?: WebhookSecurityContext;
|
|
166
225
|
}
|
|
167
226
|
export interface ApiError {
|
|
168
227
|
message?: string;
|
|
@@ -202,7 +261,7 @@ export interface SearchResult {
|
|
|
202
261
|
attachmentCount?: number;
|
|
203
262
|
};
|
|
204
263
|
}
|
|
205
|
-
export interface
|
|
264
|
+
export interface ThreadMetadata {
|
|
206
265
|
subject: string;
|
|
207
266
|
organizationId: string;
|
|
208
267
|
inboxId: string;
|
|
@@ -219,7 +278,7 @@ export interface IndexConversationPayload {
|
|
|
219
278
|
id: string;
|
|
220
279
|
subject: string;
|
|
221
280
|
content: string;
|
|
222
|
-
metadata:
|
|
281
|
+
metadata: ThreadMetadata;
|
|
223
282
|
}
|
|
224
283
|
export type SearchType = 'vector' | 'agent';
|
|
225
284
|
export interface Thread {
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -1,2 +1,50 @@
|
|
|
1
1
|
import type { SvixHeaders } from './types.js';
|
|
2
2
|
export declare const verifyResendWebhook: (payload: string, headers: SvixHeaders, secret: string) => unknown;
|
|
3
|
+
export interface CommuneWebhookHeaders {
|
|
4
|
+
/** The `x-commune-signature` header value, e.g. `"v1=5a3f2b..."` */
|
|
5
|
+
signature: string;
|
|
6
|
+
/** The `x-commune-timestamp` header value (Unix milliseconds) */
|
|
7
|
+
timestamp: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compute the expected `v1=` HMAC-SHA256 signature for a Commune webhook.
|
|
11
|
+
*
|
|
12
|
+
* Matches the backend's `computeSignature(body, timestamp, secret)`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const computeCommuneSignature: (body: string, timestamp: string, secret: string) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Verify a Commune outbound webhook signature.
|
|
17
|
+
*
|
|
18
|
+
* Commune signs every webhook delivery with HMAC-SHA256:
|
|
19
|
+
* ```
|
|
20
|
+
* x-commune-signature: v1={HMAC-SHA256(secret, "{timestamp}.{body}")}
|
|
21
|
+
* x-commune-timestamp: {unix_ms}
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param rawBody - The raw request body string
|
|
25
|
+
* @param timestamp - The `x-commune-timestamp` header (Unix ms)
|
|
26
|
+
* @param signature - The `x-commune-signature` header (`v1=...`)
|
|
27
|
+
* @param secret - Your inbox webhook secret
|
|
28
|
+
* @param toleranceMs - Max age in milliseconds (default 300000 = 5 min)
|
|
29
|
+
* @returns `true` if valid
|
|
30
|
+
* @throws Error if signature is invalid or timestamp is too old
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { verifyCommuneWebhook } from "commune-ai";
|
|
35
|
+
*
|
|
36
|
+
* const isValid = verifyCommuneWebhook({
|
|
37
|
+
* rawBody: req.body,
|
|
38
|
+
* timestamp: req.headers["x-commune-timestamp"],
|
|
39
|
+
* signature: req.headers["x-commune-signature"],
|
|
40
|
+
* secret: process.env.COMMUNE_WEBHOOK_SECRET!,
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare const verifyCommuneWebhook: (params: {
|
|
45
|
+
rawBody: string;
|
|
46
|
+
timestamp: string;
|
|
47
|
+
signature: string;
|
|
48
|
+
secret: string;
|
|
49
|
+
toleranceMs?: number;
|
|
50
|
+
}) => boolean;
|
package/dist/webhooks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
1
2
|
import { Webhook } from 'svix';
|
|
2
3
|
export const verifyResendWebhook = (payload, headers, secret) => {
|
|
3
4
|
const webhook = new Webhook(secret);
|
|
@@ -8,3 +9,67 @@ export const verifyResendWebhook = (payload, headers, secret) => {
|
|
|
8
9
|
};
|
|
9
10
|
return webhook.verify(payload, svixHeaders);
|
|
10
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Compute the expected `v1=` HMAC-SHA256 signature for a Commune webhook.
|
|
14
|
+
*
|
|
15
|
+
* Matches the backend's `computeSignature(body, timestamp, secret)`.
|
|
16
|
+
*/
|
|
17
|
+
export const computeCommuneSignature = (body, timestamp, secret) => {
|
|
18
|
+
const digest = crypto
|
|
19
|
+
.createHmac('sha256', secret)
|
|
20
|
+
.update(`${timestamp}.${body}`, 'utf8')
|
|
21
|
+
.digest('hex');
|
|
22
|
+
return `v1=${digest}`;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Verify a Commune outbound webhook signature.
|
|
26
|
+
*
|
|
27
|
+
* Commune signs every webhook delivery with HMAC-SHA256:
|
|
28
|
+
* ```
|
|
29
|
+
* x-commune-signature: v1={HMAC-SHA256(secret, "{timestamp}.{body}")}
|
|
30
|
+
* x-commune-timestamp: {unix_ms}
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @param rawBody - The raw request body string
|
|
34
|
+
* @param timestamp - The `x-commune-timestamp` header (Unix ms)
|
|
35
|
+
* @param signature - The `x-commune-signature` header (`v1=...`)
|
|
36
|
+
* @param secret - Your inbox webhook secret
|
|
37
|
+
* @param toleranceMs - Max age in milliseconds (default 300000 = 5 min)
|
|
38
|
+
* @returns `true` if valid
|
|
39
|
+
* @throws Error if signature is invalid or timestamp is too old
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { verifyCommuneWebhook } from "commune-ai";
|
|
44
|
+
*
|
|
45
|
+
* const isValid = verifyCommuneWebhook({
|
|
46
|
+
* rawBody: req.body,
|
|
47
|
+
* timestamp: req.headers["x-commune-timestamp"],
|
|
48
|
+
* signature: req.headers["x-commune-signature"],
|
|
49
|
+
* secret: process.env.COMMUNE_WEBHOOK_SECRET!,
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const verifyCommuneWebhook = (params) => {
|
|
54
|
+
const { rawBody, timestamp, signature, secret, toleranceMs = 5 * 60 * 1000 } = params;
|
|
55
|
+
if (!signature)
|
|
56
|
+
throw new Error('Missing x-commune-signature header');
|
|
57
|
+
if (!timestamp)
|
|
58
|
+
throw new Error('Missing x-commune-timestamp header');
|
|
59
|
+
if (!secret)
|
|
60
|
+
throw new Error('Missing webhook secret');
|
|
61
|
+
const expected = computeCommuneSignature(rawBody, timestamp, secret);
|
|
62
|
+
// Constant-time comparison
|
|
63
|
+
if (expected.length !== signature.length) {
|
|
64
|
+
throw new Error('Invalid webhook signature');
|
|
65
|
+
}
|
|
66
|
+
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
|
|
67
|
+
throw new Error('Invalid webhook signature');
|
|
68
|
+
}
|
|
69
|
+
// Replay protection
|
|
70
|
+
const age = Date.now() - parseInt(timestamp, 10);
|
|
71
|
+
if (Number.isNaN(age) || age > toleranceMs) {
|
|
72
|
+
throw new Error(`Webhook timestamp too old (${Math.round(age / 1000)}s)`);
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commune-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Email infrastructure for agents — set up an inbox and send your first email in 30 seconds. Programmatic inboxes (~1 line), consistent threads, custom domains, attachments, and structured data.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|