commune-ai 0.2.66 → 0.3.1

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
@@ -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
- // conversation_id: "thread_id",
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
- conversation_id: message.conversation_id,
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
- conversation_id: string; // email thread
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 `/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.
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.listByConversation(message.conversation_id, {
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
- conversation_id: message.conversation_id,
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
- conversation_id: message.conversation_id,
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
- conversation_id: message.conversation_id,
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
@@ -1,4 +1,4 @@
1
- import type { AttachmentRecord, AttachmentUrl, AttachmentUploadResponse, CreateDomainPayload, DomainEntry, InboxEntry, MessageListParams, SendMessagePayload, UnifiedMessage, SearchFilter, SearchOptions, SearchResult, IndexConversationPayload, ThreadListParams, ThreadListResponse } from './types.js';
1
+ import type { AttachmentRecord, AttachmentUrl, AttachmentUploadResponse, CreateDomainPayload, DomainEntry, InboxEntry, MessageListParams, SendMessagePayload, UnifiedMessage, SearchFilter, SearchOptions, SearchResult, IndexConversationPayload, ThreadListParams, ThreadListResponse, SearchThreadsParams, SearchThreadResult, ThreadMetadataEntry, DeliveryMetricsParams, DeliveryEventEntry, DeliveryEventsParams, DeliverySuppressionsParams, SuppressionEntry } from './types.js';
2
2
  export type ClientOptions = {
3
3
  baseUrl?: string;
4
4
  apiKey: string;
@@ -6,12 +6,13 @@ export type ClientOptions = {
6
6
  fetcher?: typeof fetch;
7
7
  };
8
8
  export declare class CommuneClient {
9
- private searchClient;
10
9
  private baseUrl;
11
10
  private apiKey;
12
11
  private headers?;
13
12
  private fetcher;
13
+ private deprecationWarnings;
14
14
  constructor(options: ClientOptions);
15
+ private warnDeprecated;
15
16
  private request;
16
17
  domains: {
17
18
  list: () => Promise<DomainEntry[]>;
@@ -22,9 +23,37 @@ export declare class CommuneClient {
22
23
  status: (domainId: string) => Promise<Record<string, unknown>>;
23
24
  };
24
25
  inboxes: {
25
- list: (domainId: string) => Promise<InboxEntry[]>;
26
- create: (domainId: string, payload: {
26
+ /**
27
+ * List inboxes.
28
+ *
29
+ * @param domainId - Optional domain ID to filter by. If omitted, lists all inboxes across all domains.
30
+ * @example
31
+ * // List all inboxes
32
+ * const allInboxes = await commune.inboxes.list();
33
+ *
34
+ * // List inboxes for a specific domain
35
+ * const domainInboxes = await commune.inboxes.list('domain-id');
36
+ */
37
+ list: (domainId?: string) => Promise<InboxEntry[]>;
38
+ /**
39
+ * Create a new inbox.
40
+ *
41
+ * @param payload - Inbox configuration
42
+ * @example
43
+ * // Simple creation with auto-resolved domain (recommended)
44
+ * const inbox = await commune.inboxes.create({
45
+ * localPart: 'support',
46
+ * });
47
+ *
48
+ * // With explicit domain (for custom domains)
49
+ * const inbox = await commune.inboxes.create({
50
+ * localPart: 'support',
51
+ * domainId: 'my-domain-id',
52
+ * });
53
+ */
54
+ create: (payload: {
27
55
  localPart: string;
56
+ domainId?: string;
28
57
  agent?: InboxEntry["agent"];
29
58
  webhook?: InboxEntry["webhook"];
30
59
  status?: string;
@@ -60,7 +89,13 @@ export declare class CommuneClient {
60
89
  messages: {
61
90
  send: (payload: SendMessagePayload) => Promise<Record<string, unknown>>;
62
91
  list: (params: MessageListParams) => Promise<UnifiedMessage[]>;
63
- listByConversation: (conversationId: string, params?: MessageListParams) => Promise<UnifiedMessage[]>;
92
+ listByThread: (threadId: string, params?: {
93
+ limit?: number;
94
+ order?: "asc" | "desc";
95
+ }) => Promise<UnifiedMessage[]>;
96
+ };
97
+ search: {
98
+ threads: (params: SearchThreadsParams) => Promise<SearchThreadResult[]>;
64
99
  };
65
100
  conversations: {
66
101
  search: (query: string, filter: SearchFilter, options?: SearchOptions) => Promise<SearchResult[]>;
@@ -77,6 +112,16 @@ export declare class CommuneClient {
77
112
  limit?: number;
78
113
  order?: "asc" | "desc";
79
114
  }) => Promise<UnifiedMessage[]>;
115
+ metadata: (threadId: string) => Promise<ThreadMetadataEntry>;
116
+ setStatus: (threadId: string, status: "open" | "needs_reply" | "waiting" | "closed") => Promise<ThreadMetadataEntry>;
117
+ addTags: (threadId: string, tags: string[]) => Promise<ThreadMetadataEntry>;
118
+ removeTags: (threadId: string, tags: string[]) => Promise<ThreadMetadataEntry>;
119
+ assign: (threadId: string, assignedTo?: string | null) => Promise<ThreadMetadataEntry>;
120
+ };
121
+ delivery: {
122
+ metrics: (params?: DeliveryMetricsParams) => Promise<Record<string, unknown>>;
123
+ events: (params?: DeliveryEventsParams) => Promise<DeliveryEventEntry[]>;
124
+ suppressions: (params?: DeliverySuppressionsParams) => Promise<SuppressionEntry[]>;
80
125
  };
81
126
  attachments: {
82
127
  upload: (content: string, filename: string, mimeType: string) => Promise<AttachmentUploadResponse>;
package/dist/client.js CHANGED
@@ -1,5 +1,4 @@
1
- import { SearchClient } from './client/search.js';
2
- const DEFAULT_BASE_URL = 'https://web-production-3f46f.up.railway.app';
1
+ const DEFAULT_BASE_URL = 'https://api.commune.email';
3
2
  const buildQuery = (params) => {
4
3
  const query = new URLSearchParams();
5
4
  Object.entries(params).forEach(([key, value]) => {
@@ -13,73 +12,113 @@ const buildQuery = (params) => {
13
12
  };
14
13
  export class CommuneClient {
15
14
  constructor(options) {
15
+ this.deprecationWarnings = new Set();
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`);
56
+ if (domainId) {
57
+ return this.request(`/v1/domains/${encodeURIComponent(domainId)}/inboxes`);
58
+ }
59
+ return this.request(`/v1/inboxes`);
46
60
  },
47
- create: async (domainId, payload) => {
48
- return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes`, {
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,26 +129,37 @@ 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
  })}`);
98
137
  },
99
138
  };
139
+ this.search = {
140
+ threads: async (params) => {
141
+ return this.request(`/v1/search/threads${buildQuery({
142
+ q: params.query,
143
+ inbox_id: params.inboxId,
144
+ domain_id: params.domainId,
145
+ limit: params.limit,
146
+ })}`);
147
+ },
148
+ };
100
149
  this.conversations = {
101
150
  search: async (query, filter, options) => {
102
- return this.request('/api/search', {
103
- method: 'POST',
104
- json: {
105
- query,
106
- filter,
107
- options,
108
- },
151
+ this.warnDeprecated('conversations.search', '[commune-ai] `conversations.search` is deprecated. Use `search.threads({ query, inboxId, domainId, limit })`.');
152
+ const inboxId = filter.inboxIds && filter.inboxIds.length > 0 ? filter.inboxIds[0] : undefined;
153
+ return this.search.threads({
154
+ query,
155
+ inboxId,
156
+ domainId: filter.domainId,
157
+ limit: options?.limit,
109
158
  });
110
159
  },
111
160
  index: async (organizationId, conversation) => {
112
- return this.request('/api/search/index', {
161
+ this.warnDeprecated('conversations.index', '[commune-ai] `conversations.index` is deprecated and will be removed. Indexing is handled automatically.');
162
+ return this.request('/v1/search/index', {
113
163
  method: 'POST',
114
164
  json: {
115
165
  organizationId,
@@ -118,7 +168,8 @@ export class CommuneClient {
118
168
  });
119
169
  },
120
170
  indexBatch: async (organizationId, conversations) => {
121
- return this.request('/api/search/index/batch', {
171
+ this.warnDeprecated('conversations.indexBatch', '[commune-ai] `conversations.indexBatch` is deprecated and will be removed. Indexing is handled automatically.');
172
+ return this.request('/v1/search/index/batch', {
122
173
  method: 'POST',
123
174
  json: {
124
175
  organizationId,
@@ -154,10 +205,62 @@ export class CommuneClient {
154
205
  order: params?.order,
155
206
  })}`);
156
207
  },
208
+ metadata: async (threadId) => {
209
+ return this.request(`/v1/threads/${encodeURIComponent(threadId)}/metadata`);
210
+ },
211
+ setStatus: async (threadId, status) => {
212
+ return this.request(`/v1/threads/${encodeURIComponent(threadId)}/status`, {
213
+ method: 'PUT',
214
+ json: { status },
215
+ });
216
+ },
217
+ addTags: async (threadId, tags) => {
218
+ return this.request(`/v1/threads/${encodeURIComponent(threadId)}/tags`, {
219
+ method: 'POST',
220
+ json: { tags },
221
+ });
222
+ },
223
+ removeTags: async (threadId, tags) => {
224
+ return this.request(`/v1/threads/${encodeURIComponent(threadId)}/tags`, {
225
+ method: 'DELETE',
226
+ json: { tags },
227
+ });
228
+ },
229
+ assign: async (threadId, assignedTo) => {
230
+ return this.request(`/v1/threads/${encodeURIComponent(threadId)}/assign`, {
231
+ method: 'PUT',
232
+ json: { assigned_to: assignedTo ?? null },
233
+ });
234
+ },
235
+ };
236
+ this.delivery = {
237
+ metrics: async (params = {}) => {
238
+ return this.request(`/v1/delivery/metrics${buildQuery({
239
+ inbox_id: params.inboxId,
240
+ domain_id: params.domainId,
241
+ period: params.period,
242
+ })}`);
243
+ },
244
+ events: async (params = {}) => {
245
+ return this.request(`/v1/delivery/events${buildQuery({
246
+ message_id: params.messageId,
247
+ inbox_id: params.inboxId,
248
+ domain_id: params.domainId,
249
+ event_type: params.eventType,
250
+ limit: params.limit,
251
+ })}`);
252
+ },
253
+ suppressions: async (params = {}) => {
254
+ return this.request(`/v1/delivery/suppressions${buildQuery({
255
+ inbox_id: params.inboxId,
256
+ domain_id: params.domainId,
257
+ limit: params.limit,
258
+ })}`);
259
+ },
157
260
  };
158
261
  this.attachments = {
159
262
  upload: async (content, filename, mimeType) => {
160
- return this.request('/api/attachments/upload', {
263
+ return this.request('/v1/attachments/upload', {
161
264
  method: 'POST',
162
265
  body: JSON.stringify({ content, filename, mimeType }),
163
266
  });
@@ -165,16 +268,21 @@ export class CommuneClient {
165
268
  get: async (attachmentId, options) => {
166
269
  if (options?.url) {
167
270
  const expiresIn = options.expiresIn || 3600;
168
- return this.request(`/api/attachments/${encodeURIComponent(attachmentId)}/url?expiresIn=${expiresIn}`);
271
+ return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}/url?expires_in=${expiresIn}`);
169
272
  }
170
- return this.request(`/api/attachments/${encodeURIComponent(attachmentId)}`);
273
+ return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}`);
171
274
  },
172
275
  };
173
276
  this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
174
277
  this.apiKey = options.apiKey;
175
278
  this.headers = options.headers;
176
279
  this.fetcher = options.fetcher || fetch;
177
- this.searchClient = new SearchClient(this.baseUrl, { Authorization: `Bearer ${this.apiKey}` });
280
+ }
281
+ warnDeprecated(key, message) {
282
+ if (this.deprecationWarnings.has(key))
283
+ return;
284
+ this.deprecationWarnings.add(key);
285
+ console.warn(message);
178
286
  }
179
287
  async request(path, options = {}) {
180
288
  const { json, headers, ...rest } = options;
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, SearchThreadResult, SearchThreadsParams, ThreadMetadataEntry, SvixHeaders, SuppressionEntry, Thread, DeliveryEventEntry, DeliveryEventsParams, DeliveryMetricsParams, DeliverySuppressionsParams, 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,7 +278,7 @@ 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';
225
284
  export interface Thread {
@@ -246,3 +305,65 @@ export interface ThreadListParams {
246
305
  cursor?: string;
247
306
  order?: 'asc' | 'desc';
248
307
  }
308
+ export interface SearchThreadsParams {
309
+ query: string;
310
+ inboxId?: string;
311
+ domainId?: string;
312
+ limit?: number;
313
+ }
314
+ export interface SearchThreadResult {
315
+ thread_id: string;
316
+ subject?: string | null;
317
+ score?: number;
318
+ inbox_id?: string | null;
319
+ domain_id?: string | null;
320
+ participants?: string[];
321
+ direction?: Direction | null;
322
+ }
323
+ export interface ThreadMetadataEntry {
324
+ thread_id: string;
325
+ orgId?: string;
326
+ tags: string[];
327
+ status: 'open' | 'needs_reply' | 'waiting' | 'closed';
328
+ assigned_to?: string | null;
329
+ updated_at?: string;
330
+ }
331
+ export interface DeliveryMetricsParams {
332
+ inboxId?: string;
333
+ domainId?: string;
334
+ period?: string;
335
+ }
336
+ export interface DeliveryEventEntry {
337
+ _id?: string;
338
+ message_id: string;
339
+ event_type: string;
340
+ event_data: Record<string, unknown>;
341
+ processed_at?: string;
342
+ inbox_id?: string;
343
+ domain_id?: string;
344
+ }
345
+ export interface DeliveryEventsParams {
346
+ messageId?: string;
347
+ inboxId?: string;
348
+ domainId?: string;
349
+ eventType?: string;
350
+ limit?: number;
351
+ }
352
+ export interface SuppressionEntry {
353
+ _id?: string;
354
+ email: string;
355
+ reason: string;
356
+ type: string;
357
+ source: 'inbox' | 'domain' | 'global';
358
+ inbox_id?: string;
359
+ domain_id?: string;
360
+ created_at?: string;
361
+ expires_at?: string;
362
+ message_id?: string;
363
+ metadata?: Record<string, unknown>;
364
+ }
365
+ export interface DeliverySuppressionsParams {
366
+ inboxId?: string;
367
+ domainId?: string;
368
+ limit?: number;
369
+ }
@@ -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.2.66",
3
+ "version": "0.3.1",
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",