commune-ai 0.2.65 → 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 CHANGED
@@ -1,27 +1,25 @@
1
1
  # commune-ai
2
2
 
3
- Commune is the **communication infrastructure for agents**. It gives your agent a **unified inbox**
4
- for **email**, so your agent can talk to humans where they already work. Most teams
5
- get a working integration in **~15 minutes**.
3
+ Email infrastructure for agents set up an inbox and send your first email in 30 seconds. Programmatic inboxes (~1 line), consistent threads, setup and verify custom domains, send and receive attachments, structured data extraction.
6
4
 
7
- ## Why Commune exists (what it enables)
8
- Agents are powerful, but users already live in **email**. Commune bridges that gap so:
9
- - your agent is reachable where humans already work
10
- - you don’t have to build deliverability, threading, or email plumbing
11
- - you can ship an agent‑first experience in minutes, not weeks
12
-
13
- In practice, Commune lets you:
14
- - give an agent a real inbox on your domain
15
- - respond in the correct email thread every time
16
- - use **conversation state** to make smarter, context‑aware replies
17
-
18
- ## How it works (mental model)
19
- 1) Commune receives inbound email events.
20
- 2) Commune normalizes them into a **UnifiedMessage**.
21
- 3) Commune sends the UnifiedMessage to your webhook.
22
- 4) Your agent replies using one API call.
5
+ ```bash
6
+ npm install commune-ai
7
+ ```
23
8
 
24
- > By default, the SDK talks to the hosted Commune API. If you self‑host,\n> pass `baseUrl` to the client.
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)
25
23
 
26
24
  ---
27
25
 
@@ -51,7 +49,7 @@ const handler = createWebhookHandler({
51
49
  // Example inbound payload:
52
50
  // message = {
53
51
  // channel: "email",
54
- // conversation_id: "thread_id",
52
+ // thread_id: "thread_abc123",
55
53
  // participants: [{ role: "sender", identity: "user@example.com" }],
56
54
  // content: "Can you help with pricing?"
57
55
  // }
@@ -68,8 +66,7 @@ const handler = createWebhookHandler({
68
66
  channel: "email",
69
67
  to: sender,
70
68
  text: agentReply,
71
- conversation_id: message.conversation_id,
72
- domainId: context.payload.domainId,
69
+ thread_id: message.thread_id,
73
70
  inboxId: context.payload.inboxId,
74
71
  });
75
72
  },
@@ -94,7 +91,7 @@ Every inbound email arrives in this shape:
94
91
  export interface UnifiedMessage {
95
92
  channel: "email";
96
93
  message_id: string;
97
- conversation_id: string; // email thread
94
+ thread_id: string; // email thread
98
95
  participants: { role: string; identity: string }[];
99
96
  content: string;
100
97
  metadata: { ... };
@@ -104,7 +101,7 @@ export interface UnifiedMessage {
104
101
  ---
105
102
 
106
103
  ## API key (required)
107
- All `/api/*` requests require an API key. Create one in the dashboard and reuse it in your client.
104
+ All `/v1/*` requests require an API key. Create one in the dashboard and reuse it in your client.
108
105
 
109
106
  ```bash
110
107
  export COMMUNE_API_KEY="your_key_from_dashboard"
@@ -318,48 +315,6 @@ for (const result of withAttachments) {
318
315
  }
319
316
  ```
320
317
 
321
- ## Spam Protection
322
- Commune includes built-in spam detection and protection to keep your agent safe from malicious emails and prevent abuse.
323
-
324
- ### Automatic Spam Detection
325
- All incoming emails are automatically analyzed for spam patterns:
326
- - Content analysis (spam keywords, suspicious patterns)
327
- - URL validation (broken links, phishing detection)
328
- - Sender reputation tracking
329
- - Domain blacklist checking
330
-
331
- Spam emails are automatically rejected before reaching your webhook, protecting your agent from:
332
- - Phishing attempts
333
- - Malicious links
334
- - Mass spam campaigns
335
- - Fraudulent content
336
-
337
- ### Outbound Protection
338
- Commune also protects your sending reputation:
339
- - Rate limiting per organization tier
340
- - Content validation for outbound emails
341
- - Burst detection for mass mailing
342
- - Automatic blocking of spam-like content
343
-
344
- This ensures your domain maintains high deliverability and isn't flagged by email providers.
345
-
346
- ### Spam Reporting
347
- If a spam email gets through, you can report it:
348
-
349
- ```ts
350
- import { CommuneClient } from "commune-ai";
351
- const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
352
-
353
- // Report a message as spam
354
- await client.reportSpam({
355
- message_id: "msg_123",
356
- reason: "Unsolicited marketing email",
357
- classification: "spam" // or "phishing", "malware", "other"
358
- });
359
- ```
360
-
361
- The system learns from reports and automatically blocks repeat offenders.
362
-
363
318
  ## Context (conversation state)
364
319
  Commune stores conversation state so your agent can respond with context.
365
320
 
@@ -368,7 +323,7 @@ import { CommuneClient } from "commune-ai";
368
323
  const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
369
324
 
370
325
  // Thread history (email thread)
371
- const thread = await client.messages.listByConversation(message.conversation_id, {
326
+ const thread = await client.messages.listByThread(message.thread_id, {
372
327
  order: "asc",
373
328
  limit: 50,
374
329
  });
@@ -408,8 +363,7 @@ const handler = createWebhookHandler({
408
363
  channel: "email",
409
364
  to: sender,
410
365
  text: "Got it — thanks for the message.",
411
- conversation_id: message.conversation_id,
412
- domainId: context.payload.domainId,
366
+ thread_id: message.thread_id,
413
367
  inboxId: context.payload.inboxId,
414
368
  });
415
369
  },
@@ -449,8 +403,7 @@ await client.messages.send({
449
403
  channel: "email",
450
404
  to: "user@example.com",
451
405
  text: "Thanks — replying in thread.",
452
- conversation_id: message.conversation_id,
453
- domainId: context.payload.domainId,
406
+ thread_id: message.thread_id,
454
407
  inboxId: context.payload.inboxId,
455
408
  });
456
409
  ```
@@ -645,8 +598,7 @@ const handler = createWebhookHandler({
645
598
  subject: "Your receipt",
646
599
  text: "Thanks! Here's your receipt.",
647
600
  attachments: [attachment_id],
648
- conversation_id: message.conversation_id,
649
- domainId: context.payload.domainId,
601
+ thread_id: message.thread_id,
650
602
  inboxId: context.payload.inboxId,
651
603
  });
652
604
  },
@@ -656,3 +608,113 @@ const app = express();
656
608
  app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
657
609
  app.listen(3000, () => console.log("listening on 3000"));
658
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
@@ -1,4 +1,4 @@
1
- import type { AttachmentRecord, AttachmentUrl, AttachmentUploadResponse, CreateDomainPayload, DomainEntry, InboxEntry, MessageListParams, SendMessagePayload, UnifiedMessage, SearchFilter, SearchOptions, SearchResult, IndexConversationPayload } from './types.js';
1
+ import type { AttachmentRecord, AttachmentUrl, AttachmentUploadResponse, CreateDomainPayload, DomainEntry, InboxEntry, MessageListParams, SendMessagePayload, UnifiedMessage, SearchFilter, SearchOptions, SearchResult, IndexConversationPayload, ThreadListParams, ThreadListResponse } from './types.js';
2
2
  export type ClientOptions = {
3
3
  baseUrl?: string;
4
4
  apiKey: string;
@@ -22,9 +22,37 @@ export declare class CommuneClient {
22
22
  status: (domainId: string) => Promise<Record<string, unknown>>;
23
23
  };
24
24
  inboxes: {
25
- list: (domainId: string) => Promise<InboxEntry[]>;
26
- create: (domainId: string, payload: {
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
- listByConversation: (conversationId: string, params?: MessageListParams) => Promise<UnifiedMessage[]>;
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[]>;
@@ -71,6 +102,13 @@ export declare class CommuneClient {
71
102
  success: boolean;
72
103
  }>;
73
104
  };
105
+ threads: {
106
+ list: (params: ThreadListParams) => Promise<ThreadListResponse>;
107
+ messages: (threadId: string, params?: {
108
+ limit?: number;
109
+ order?: "asc" | "desc";
110
+ }) => Promise<UnifiedMessage[]>;
111
+ };
74
112
  attachments: {
75
113
  upload: (content: string, filename: string, mimeType: string) => Promise<AttachmentUploadResponse>;
76
114
  get: (attachmentId: string, options?: {
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SearchClient } from './client/search.js';
2
- const DEFAULT_BASE_URL = 'https://web-production-3f46f.up.railway.app';
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(`/api/domains`);
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(`/api/domains`, {
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(`/api/domains/${encodeURIComponent(domainId)}`);
31
+ return this.request(`/v1/domains/${encodeURIComponent(domainId)}`);
32
32
  },
33
33
  verify: async (domainId) => {
34
- return this.request(`/api/domains/${encodeURIComponent(domainId)}/verify`, { method: 'POST' });
34
+ return this.request(`/v1/domains/${encodeURIComponent(domainId)}/verify`, { method: 'POST' });
35
35
  },
36
36
  records: async (domainId) => {
37
- return this.request(`/api/domains/${encodeURIComponent(domainId)}/records`);
37
+ return this.request(`/v1/domains/${encodeURIComponent(domainId)}/records`);
38
38
  },
39
39
  status: async (domainId) => {
40
- return this.request(`/api/domains/${encodeURIComponent(domainId)}/status`);
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
- return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes`);
46
- },
47
- create: async (domainId, payload) => {
48
- return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes`, {
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(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}`, {
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(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}`, { method: 'DELETE' });
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(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/webhook`, { method: 'POST', json: payload });
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(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/extraction-schema`, { method: 'PUT', json: schema });
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(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/extraction-schema`, { method: 'DELETE' });
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('/api/messages/send', {
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(`/api/messages${buildQuery({
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
- listByConversation: async (conversationId, params) => {
94
- return this.request(`/api/conversations/${encodeURIComponent(conversationId)}/messages${buildQuery({
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('/api/search', {
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('/api/search/index', {
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('/api/search/index/batch', {
160
+ return this.request('/v1/search/index/batch', {
122
161
  method: 'POST',
123
162
  json: {
124
163
  organizationId,
@@ -127,9 +166,37 @@ export class CommuneClient {
127
166
  });
128
167
  },
129
168
  };
169
+ this.threads = {
170
+ list: async (params) => {
171
+ const response = await this.fetcher(`${this.baseUrl}/v1/threads${buildQuery({
172
+ inbox_id: params.inbox_id,
173
+ domain_id: params.domain_id,
174
+ limit: params.limit,
175
+ cursor: params.cursor,
176
+ order: params.order,
177
+ })}`, {
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
181
+ ...(this.headers || {}),
182
+ },
183
+ });
184
+ const data = await response.json();
185
+ if (!response.ok) {
186
+ throw new Error(data?.error || response.statusText);
187
+ }
188
+ return data;
189
+ },
190
+ messages: async (threadId, params) => {
191
+ return this.request(`/v1/threads/${encodeURIComponent(threadId)}/messages${buildQuery({
192
+ limit: params?.limit,
193
+ order: params?.order,
194
+ })}`);
195
+ },
196
+ };
130
197
  this.attachments = {
131
198
  upload: async (content, filename, mimeType) => {
132
- return this.request('/api/attachments/upload', {
199
+ return this.request('/v1/attachments/upload', {
133
200
  method: 'POST',
134
201
  body: JSON.stringify({ content, filename, mimeType }),
135
202
  });
@@ -137,9 +204,9 @@ export class CommuneClient {
137
204
  get: async (attachmentId, options) => {
138
205
  if (options?.url) {
139
206
  const expiresIn = options.expiresIn || 3600;
140
- return this.request(`/api/attachments/${encodeURIComponent(attachmentId)}/url?expiresIn=${expiresIn}`);
207
+ return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}/url?expires_in=${expiresIn}`);
141
208
  }
142
- return this.request(`/api/attachments/${encodeURIComponent(attachmentId)}`);
209
+ return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}`);
143
210
  },
144
211
  };
145
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, 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
@@ -1,3 +1,3 @@
1
1
  export { CommuneClient } from './client.js';
2
- export { verifyResendWebhook } from './webhooks.js';
2
+ export { verifyResendWebhook, verifyCommuneWebhook, computeCommuneSignature } from './webhooks.js';
3
3
  export { createWebhookHandler } from './listener.js';
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
- conversation_id: string;
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
- conversation_id?: string;
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 ConversationMetadata {
264
+ export interface ThreadMetadata {
206
265
  subject: string;
207
266
  organizationId: string;
208
267
  inboxId: string;
@@ -219,6 +278,30 @@ export interface IndexConversationPayload {
219
278
  id: string;
220
279
  subject: string;
221
280
  content: string;
222
- metadata: ConversationMetadata;
281
+ metadata: ThreadMetadata;
223
282
  }
224
283
  export type SearchType = 'vector' | 'agent';
284
+ export interface Thread {
285
+ thread_id: string;
286
+ subject?: string | null;
287
+ last_message_at: string;
288
+ first_message_at?: string | null;
289
+ message_count: number;
290
+ snippet?: string | null;
291
+ last_direction?: Direction | null;
292
+ inbox_id?: string | null;
293
+ domain_id?: string | null;
294
+ has_attachments?: boolean;
295
+ }
296
+ export interface ThreadListResponse {
297
+ data: Thread[];
298
+ next_cursor: string | null;
299
+ has_more: boolean;
300
+ }
301
+ export interface ThreadListParams {
302
+ inbox_id?: string;
303
+ domain_id?: string;
304
+ limit?: number;
305
+ cursor?: string;
306
+ order?: 'asc' | 'desc';
307
+ }
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "commune-ai",
3
- "version": "0.2.65",
4
- "description": "SDK-first email infrastructure for agents - threads, history, semantic search, structured data output and attachments.",
3
+ "version": "0.3.0",
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",
7
7
  "types": "dist/index.d.ts",