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 +136 -74
- package/dist/client.d.ts +42 -4
- package/dist/client.js +93 -26
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +87 -4
- package/dist/webhooks.d.ts +48 -0
- package/dist/webhooks.js +65 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
# commune-ai
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 `/
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[]>;
|
|
@@ -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://
|
|
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,
|
|
@@ -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('/
|
|
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(`/
|
|
207
|
+
return this.request(`/v1/attachments/${encodeURIComponent(attachmentId)}/url?expires_in=${expiresIn}`);
|
|
141
208
|
}
|
|
142
|
-
return this.request(`/
|
|
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
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,6 +278,30 @@ 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';
|
|
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
|
+
}
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commune-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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",
|