@contractspec/integration.runtime 2.9.0 → 3.0.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.
Files changed (65) hide show
  1. package/dist/channel/dispatcher.d.ts +37 -0
  2. package/dist/channel/dispatcher.js +130 -0
  3. package/dist/channel/dispatcher.test.d.ts +1 -0
  4. package/dist/channel/github.d.ts +47 -0
  5. package/dist/channel/github.js +58 -0
  6. package/dist/channel/github.test.d.ts +1 -0
  7. package/dist/channel/index.d.ts +14 -0
  8. package/dist/channel/index.js +1420 -0
  9. package/dist/channel/memory-store.d.ts +28 -0
  10. package/dist/channel/memory-store.js +223 -0
  11. package/dist/channel/policy.d.ts +19 -0
  12. package/dist/channel/policy.js +110 -0
  13. package/dist/channel/policy.test.d.ts +1 -0
  14. package/dist/channel/postgres-queries.d.ts +11 -0
  15. package/dist/channel/postgres-queries.js +222 -0
  16. package/dist/channel/postgres-schema.d.ts +1 -0
  17. package/dist/channel/postgres-schema.js +94 -0
  18. package/dist/channel/postgres-store.d.ts +21 -0
  19. package/dist/channel/postgres-store.js +498 -0
  20. package/dist/channel/postgres-store.test.d.ts +1 -0
  21. package/dist/channel/replay-fixtures.d.ts +8 -0
  22. package/dist/channel/replay-fixtures.js +31 -0
  23. package/dist/channel/replay.test.d.ts +1 -0
  24. package/dist/channel/service.d.ts +26 -0
  25. package/dist/channel/service.js +287 -0
  26. package/dist/channel/service.test.d.ts +1 -0
  27. package/dist/channel/slack.d.ts +42 -0
  28. package/dist/channel/slack.js +82 -0
  29. package/dist/channel/slack.test.d.ts +1 -0
  30. package/dist/channel/store.d.ts +83 -0
  31. package/dist/channel/store.js +1 -0
  32. package/dist/channel/telemetry.d.ts +17 -0
  33. package/dist/channel/telemetry.js +1 -0
  34. package/dist/channel/types.d.ts +111 -0
  35. package/dist/channel/types.js +1 -0
  36. package/dist/channel/whatsapp-meta.d.ts +55 -0
  37. package/dist/channel/whatsapp-meta.js +66 -0
  38. package/dist/channel/whatsapp-meta.test.d.ts +1 -0
  39. package/dist/channel/whatsapp-twilio.d.ts +20 -0
  40. package/dist/channel/whatsapp-twilio.js +61 -0
  41. package/dist/channel/whatsapp-twilio.test.d.ts +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1443 -1
  44. package/dist/node/channel/dispatcher.js +129 -0
  45. package/dist/node/channel/github.js +57 -0
  46. package/dist/node/channel/index.js +1419 -0
  47. package/dist/node/channel/memory-store.js +222 -0
  48. package/dist/node/channel/policy.js +109 -0
  49. package/dist/node/channel/postgres-queries.js +221 -0
  50. package/dist/node/channel/postgres-schema.js +93 -0
  51. package/dist/node/channel/postgres-store.js +497 -0
  52. package/dist/node/channel/replay-fixtures.js +30 -0
  53. package/dist/node/channel/service.js +286 -0
  54. package/dist/node/channel/slack.js +81 -0
  55. package/dist/node/channel/store.js +0 -0
  56. package/dist/node/channel/telemetry.js +0 -0
  57. package/dist/node/channel/types.js +0 -0
  58. package/dist/node/channel/whatsapp-meta.js +65 -0
  59. package/dist/node/channel/whatsapp-twilio.js +60 -0
  60. package/dist/node/index.js +1443 -1
  61. package/dist/node/runtime.js +26 -1
  62. package/dist/runtime.d.ts +9 -0
  63. package/dist/runtime.health.test.d.ts +1 -0
  64. package/dist/runtime.js +26 -1
  65. package/package.json +213 -6
@@ -0,0 +1,287 @@
1
+ // @bun
2
+ // src/channel/policy.ts
3
+ var DEFAULT_MESSAGING_POLICY_CONFIG = {
4
+ autoResolveMinConfidence: 0.85,
5
+ assistMinConfidence: 0.65,
6
+ blockedSignals: [
7
+ "ignore previous instructions",
8
+ "reveal secret",
9
+ "api key",
10
+ "password",
11
+ "token",
12
+ "drop table",
13
+ "delete repository"
14
+ ],
15
+ highRiskSignals: [
16
+ "refund",
17
+ "delete account",
18
+ "cancel subscription",
19
+ "permission",
20
+ "admin access",
21
+ "wire transfer",
22
+ "bank account"
23
+ ],
24
+ mediumRiskSignals: [
25
+ "urgent",
26
+ "legal",
27
+ "compliance",
28
+ "frustrated",
29
+ "escalate",
30
+ "outage"
31
+ ],
32
+ safeAckTemplate: "Thanks for your message. We received it and are preparing the next step."
33
+ };
34
+
35
+ class MessagingPolicyEngine {
36
+ config;
37
+ constructor(config) {
38
+ this.config = {
39
+ ...DEFAULT_MESSAGING_POLICY_CONFIG,
40
+ ...config ?? {}
41
+ };
42
+ }
43
+ evaluate(input) {
44
+ const text = (input.event.message?.text ?? "").toLowerCase();
45
+ if (containsAny(text, this.config.blockedSignals)) {
46
+ return {
47
+ confidence: 0.2,
48
+ riskTier: "blocked",
49
+ verdict: "blocked",
50
+ reasons: ["blocked_signal_detected"],
51
+ responseText: this.config.safeAckTemplate,
52
+ requiresApproval: true
53
+ };
54
+ }
55
+ if (containsAny(text, this.config.highRiskSignals)) {
56
+ return {
57
+ confidence: 0.55,
58
+ riskTier: "high",
59
+ verdict: "assist",
60
+ reasons: ["high_risk_topic_detected"],
61
+ responseText: this.config.safeAckTemplate,
62
+ requiresApproval: true
63
+ };
64
+ }
65
+ const mediumRiskDetected = containsAny(text, this.config.mediumRiskSignals);
66
+ const confidence = mediumRiskDetected ? 0.74 : 0.92;
67
+ const riskTier = mediumRiskDetected ? "medium" : "low";
68
+ if (confidence >= this.config.autoResolveMinConfidence && riskTier === "low") {
69
+ return {
70
+ confidence,
71
+ riskTier,
72
+ verdict: "autonomous",
73
+ reasons: ["low_risk_high_confidence"],
74
+ responseText: this.defaultResponseText(input.event),
75
+ requiresApproval: false
76
+ };
77
+ }
78
+ if (confidence >= this.config.assistMinConfidence) {
79
+ return {
80
+ confidence,
81
+ riskTier,
82
+ verdict: "assist",
83
+ reasons: ["needs_human_review"],
84
+ responseText: this.config.safeAckTemplate,
85
+ requiresApproval: true
86
+ };
87
+ }
88
+ return {
89
+ confidence,
90
+ riskTier: "blocked",
91
+ verdict: "blocked",
92
+ reasons: ["low_confidence"],
93
+ responseText: this.config.safeAckTemplate,
94
+ requiresApproval: true
95
+ };
96
+ }
97
+ defaultResponseText(event) {
98
+ if (!event.message?.text) {
99
+ return this.config.safeAckTemplate;
100
+ }
101
+ return `Acknowledged: ${event.message.text.slice(0, 240)}`;
102
+ }
103
+ }
104
+ function containsAny(text, candidates) {
105
+ return candidates.some((candidate) => text.includes(candidate));
106
+ }
107
+
108
+ // src/channel/service.ts
109
+ import { createHash, randomUUID } from "crypto";
110
+ class ChannelRuntimeService {
111
+ store;
112
+ policy;
113
+ asyncProcessing;
114
+ processInBackground;
115
+ modelName;
116
+ promptVersion;
117
+ policyVersion;
118
+ telemetry;
119
+ constructor(store, options = {}) {
120
+ this.store = store;
121
+ this.policy = options.policy ?? new MessagingPolicyEngine;
122
+ this.asyncProcessing = options.asyncProcessing ?? true;
123
+ this.processInBackground = options.processInBackground ?? ((task) => {
124
+ setTimeout(() => {
125
+ task();
126
+ }, 0);
127
+ });
128
+ this.modelName = options.modelName ?? "policy-heuristics-v1";
129
+ this.promptVersion = options.promptVersion ?? "channel-runtime.v1";
130
+ this.policyVersion = options.policyVersion ?? "messaging-policy.v1";
131
+ this.telemetry = options.telemetry;
132
+ }
133
+ async ingest(event) {
134
+ const startedAtMs = Date.now();
135
+ const claim = await this.store.claimEventReceipt({
136
+ workspaceId: event.workspaceId,
137
+ providerKey: event.providerKey,
138
+ externalEventId: event.externalEventId,
139
+ eventType: event.eventType,
140
+ signatureValid: event.signatureValid,
141
+ payloadHash: event.rawPayload ? sha256(event.rawPayload) : undefined,
142
+ traceId: event.traceId
143
+ });
144
+ if (claim.duplicate) {
145
+ this.telemetry?.record({
146
+ stage: "ingest",
147
+ status: "duplicate",
148
+ workspaceId: event.workspaceId,
149
+ providerKey: event.providerKey,
150
+ receiptId: claim.receiptId,
151
+ traceId: event.traceId,
152
+ latencyMs: Date.now() - startedAtMs
153
+ });
154
+ return {
155
+ status: "duplicate",
156
+ receiptId: claim.receiptId
157
+ };
158
+ }
159
+ this.telemetry?.record({
160
+ stage: "ingest",
161
+ status: "accepted",
162
+ workspaceId: event.workspaceId,
163
+ providerKey: event.providerKey,
164
+ receiptId: claim.receiptId,
165
+ traceId: event.traceId,
166
+ latencyMs: Date.now() - startedAtMs
167
+ });
168
+ const task = async () => {
169
+ await this.processAcceptedEvent(claim.receiptId, event);
170
+ };
171
+ if (this.asyncProcessing) {
172
+ this.processInBackground(task);
173
+ } else {
174
+ await task();
175
+ }
176
+ return {
177
+ status: "accepted",
178
+ receiptId: claim.receiptId
179
+ };
180
+ }
181
+ async processAcceptedEvent(receiptId, event) {
182
+ try {
183
+ await this.store.updateReceiptStatus(receiptId, "processing");
184
+ const thread = await this.store.upsertThread({
185
+ workspaceId: event.workspaceId,
186
+ providerKey: event.providerKey,
187
+ externalThreadId: event.thread.externalThreadId,
188
+ externalChannelId: event.thread.externalChannelId,
189
+ externalUserId: event.thread.externalUserId,
190
+ occurredAt: event.occurredAt
191
+ });
192
+ const policyDecision = this.policy.evaluate({ event });
193
+ this.telemetry?.record({
194
+ stage: "decision",
195
+ status: "processed",
196
+ workspaceId: event.workspaceId,
197
+ providerKey: event.providerKey,
198
+ receiptId,
199
+ traceId: event.traceId,
200
+ metadata: {
201
+ verdict: policyDecision.verdict,
202
+ riskTier: policyDecision.riskTier,
203
+ confidence: policyDecision.confidence
204
+ }
205
+ });
206
+ const decision = await this.store.saveDecision({
207
+ receiptId,
208
+ threadId: thread.id,
209
+ policyMode: policyDecision.verdict === "autonomous" ? "autonomous" : "assist",
210
+ riskTier: policyDecision.riskTier,
211
+ confidence: policyDecision.confidence,
212
+ modelName: this.modelName,
213
+ promptVersion: this.promptVersion,
214
+ policyVersion: this.policyVersion,
215
+ actionPlan: {
216
+ verdict: policyDecision.verdict,
217
+ reasons: policyDecision.reasons
218
+ },
219
+ requiresApproval: policyDecision.requiresApproval
220
+ });
221
+ if (policyDecision.verdict === "autonomous") {
222
+ await this.store.enqueueOutboxAction({
223
+ workspaceId: event.workspaceId,
224
+ providerKey: event.providerKey,
225
+ decisionId: decision.id,
226
+ threadId: thread.id,
227
+ actionType: "reply",
228
+ idempotencyKey: buildOutboxIdempotencyKey(event, policyDecision.responseText),
229
+ target: {
230
+ externalThreadId: event.thread.externalThreadId,
231
+ externalChannelId: event.thread.externalChannelId,
232
+ externalUserId: event.thread.externalUserId
233
+ },
234
+ payload: {
235
+ id: randomUUID(),
236
+ text: policyDecision.responseText
237
+ }
238
+ });
239
+ this.telemetry?.record({
240
+ stage: "outbox",
241
+ status: "accepted",
242
+ workspaceId: event.workspaceId,
243
+ providerKey: event.providerKey,
244
+ receiptId,
245
+ traceId: event.traceId,
246
+ metadata: {
247
+ actionType: "reply"
248
+ }
249
+ });
250
+ }
251
+ await this.store.updateReceiptStatus(receiptId, "processed");
252
+ this.telemetry?.record({
253
+ stage: "ingest",
254
+ status: "processed",
255
+ workspaceId: event.workspaceId,
256
+ providerKey: event.providerKey,
257
+ receiptId,
258
+ traceId: event.traceId
259
+ });
260
+ } catch (error) {
261
+ await this.store.updateReceiptStatus(receiptId, "failed", {
262
+ code: "PROCESSING_FAILED",
263
+ message: error instanceof Error ? error.message : String(error)
264
+ });
265
+ this.telemetry?.record({
266
+ stage: "ingest",
267
+ status: "failed",
268
+ workspaceId: event.workspaceId,
269
+ providerKey: event.providerKey,
270
+ receiptId,
271
+ traceId: event.traceId,
272
+ metadata: {
273
+ errorCode: "PROCESSING_FAILED"
274
+ }
275
+ });
276
+ }
277
+ }
278
+ }
279
+ function buildOutboxIdempotencyKey(event, responseText) {
280
+ return sha256(`${event.workspaceId}:${event.providerKey}:${event.externalEventId}:reply:${responseText}`);
281
+ }
282
+ function sha256(value) {
283
+ return createHash("sha256").update(value).digest("hex");
284
+ }
285
+ export {
286
+ ChannelRuntimeService
287
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { ChannelInboundEvent } from './types';
2
+ export interface SlackWebhookEnvelope {
3
+ type: string;
4
+ team_id?: string;
5
+ api_app_id?: string;
6
+ event_id?: string;
7
+ event_time?: number;
8
+ challenge?: string;
9
+ event?: {
10
+ type?: string;
11
+ user?: string;
12
+ text?: string;
13
+ ts?: string;
14
+ thread_ts?: string;
15
+ channel?: string;
16
+ subtype?: string;
17
+ bot_id?: string;
18
+ };
19
+ }
20
+ export interface SlackSignatureVerificationInput {
21
+ signingSecret: string;
22
+ requestTimestamp: string | null;
23
+ requestSignature: string | null;
24
+ rawBody: string;
25
+ nowMs?: number;
26
+ toleranceSeconds?: number;
27
+ }
28
+ export interface SignatureVerificationResult {
29
+ valid: boolean;
30
+ reason?: string;
31
+ }
32
+ export declare function verifySlackSignature(input: SlackSignatureVerificationInput): SignatureVerificationResult;
33
+ export declare function parseSlackWebhookPayload(rawBody: string): SlackWebhookEnvelope;
34
+ export declare function isSlackUrlVerificationPayload(payload: SlackWebhookEnvelope): boolean;
35
+ export interface NormalizeSlackEventInput {
36
+ workspaceId: string;
37
+ payload: SlackWebhookEnvelope;
38
+ signatureValid: boolean;
39
+ traceId?: string;
40
+ rawBody?: string;
41
+ }
42
+ export declare function normalizeSlackInboundEvent(input: NormalizeSlackEventInput): ChannelInboundEvent | null;
@@ -0,0 +1,82 @@
1
+ // @bun
2
+ // src/channel/slack.ts
3
+ import { createHmac, timingSafeEqual } from "crypto";
4
+ function verifySlackSignature(input) {
5
+ if (!input.requestTimestamp || !input.requestSignature) {
6
+ return { valid: false, reason: "missing_signature_headers" };
7
+ }
8
+ const timestamp = Number.parseInt(input.requestTimestamp, 10);
9
+ if (!Number.isFinite(timestamp)) {
10
+ return { valid: false, reason: "invalid_timestamp" };
11
+ }
12
+ const nowMs = input.nowMs ?? Date.now();
13
+ const toleranceMs = (input.toleranceSeconds ?? 300) * 1000;
14
+ if (Math.abs(nowMs - timestamp * 1000) > toleranceMs) {
15
+ return { valid: false, reason: "timestamp_out_of_range" };
16
+ }
17
+ const base = `v0:${input.requestTimestamp}:${input.rawBody}`;
18
+ const expected = `v0=${createHmac("sha256", input.signingSecret).update(base).digest("hex")}`;
19
+ const receivedBuffer = Buffer.from(input.requestSignature, "utf8");
20
+ const expectedBuffer = Buffer.from(expected, "utf8");
21
+ if (receivedBuffer.length !== expectedBuffer.length) {
22
+ return { valid: false, reason: "signature_length_mismatch" };
23
+ }
24
+ const valid = timingSafeEqual(receivedBuffer, expectedBuffer);
25
+ return valid ? { valid: true } : { valid: false, reason: "signature_mismatch" };
26
+ }
27
+ function parseSlackWebhookPayload(rawBody) {
28
+ const parsed = JSON.parse(rawBody);
29
+ return parsed;
30
+ }
31
+ function isSlackUrlVerificationPayload(payload) {
32
+ return payload.type === "url_verification";
33
+ }
34
+ function normalizeSlackInboundEvent(input) {
35
+ if (input.payload.type !== "event_callback") {
36
+ return null;
37
+ }
38
+ const event = input.payload.event;
39
+ if (!event?.type) {
40
+ return null;
41
+ }
42
+ if (event.subtype === "bot_message" || event.bot_id) {
43
+ return null;
44
+ }
45
+ const externalEventId = input.payload.event_id ?? event.ts;
46
+ if (!externalEventId) {
47
+ return null;
48
+ }
49
+ const threadId = event.thread_ts ?? event.ts ?? externalEventId;
50
+ if (!threadId) {
51
+ return null;
52
+ }
53
+ return {
54
+ workspaceId: input.workspaceId,
55
+ providerKey: "messaging.slack",
56
+ externalEventId,
57
+ eventType: `slack.${event.type}`,
58
+ occurredAt: input.payload.event_time ? new Date(input.payload.event_time * 1000) : new Date,
59
+ signatureValid: input.signatureValid,
60
+ traceId: input.traceId,
61
+ rawPayload: input.rawBody,
62
+ thread: {
63
+ externalThreadId: threadId,
64
+ externalChannelId: event.channel,
65
+ externalUserId: event.user
66
+ },
67
+ message: event.text ? {
68
+ text: event.text,
69
+ externalMessageId: event.ts
70
+ } : undefined,
71
+ metadata: {
72
+ slackEventType: event.type,
73
+ slackTeamId: input.payload.team_id ?? input.workspaceId
74
+ }
75
+ };
76
+ }
77
+ export {
78
+ verifySlackSignature,
79
+ parseSlackWebhookPayload,
80
+ normalizeSlackInboundEvent,
81
+ isSlackUrlVerificationPayload
82
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,83 @@
1
+ import type { ChannelDecisionRecord, ChannelDeliveryAttemptRecord, ChannelEventReceiptRecord, ChannelOutboxActionRecord, ChannelThreadRecord } from './types';
2
+ export interface ClaimEventReceiptInput {
3
+ workspaceId: string;
4
+ providerKey: string;
5
+ externalEventId: string;
6
+ eventType: string;
7
+ signatureValid: boolean;
8
+ payloadHash?: string;
9
+ traceId?: string;
10
+ }
11
+ export interface ClaimEventReceiptResult {
12
+ receiptId: string;
13
+ duplicate: boolean;
14
+ }
15
+ export interface UpsertThreadInput {
16
+ workspaceId: string;
17
+ providerKey: string;
18
+ externalThreadId: string;
19
+ externalChannelId?: string;
20
+ externalUserId?: string;
21
+ state?: Record<string, unknown>;
22
+ occurredAt?: Date;
23
+ }
24
+ export interface SaveDecisionInput {
25
+ receiptId: string;
26
+ threadId: string;
27
+ policyMode: 'suggest' | 'assist' | 'autonomous';
28
+ riskTier: 'low' | 'medium' | 'high' | 'blocked';
29
+ confidence: number;
30
+ modelName: string;
31
+ promptVersion: string;
32
+ policyVersion: string;
33
+ toolTrace?: Record<string, unknown>[];
34
+ actionPlan: Record<string, unknown>;
35
+ requiresApproval: boolean;
36
+ }
37
+ export interface EnqueueOutboxActionInput {
38
+ workspaceId: string;
39
+ providerKey: string;
40
+ decisionId: string;
41
+ threadId: string;
42
+ actionType: string;
43
+ idempotencyKey: string;
44
+ target: Record<string, unknown>;
45
+ payload: Record<string, unknown>;
46
+ }
47
+ export interface EnqueueOutboxActionResult {
48
+ actionId: string;
49
+ duplicate: boolean;
50
+ }
51
+ export interface RecordDeliveryAttemptInput {
52
+ actionId: string;
53
+ attempt: number;
54
+ responseStatus?: number;
55
+ responseBody?: string;
56
+ latencyMs?: number;
57
+ }
58
+ export interface MarkOutboxRetryInput {
59
+ actionId: string;
60
+ nextAttemptAt: Date;
61
+ lastErrorCode: string;
62
+ lastErrorMessage: string;
63
+ }
64
+ export interface MarkOutboxDeadLetterInput {
65
+ actionId: string;
66
+ lastErrorCode: string;
67
+ lastErrorMessage: string;
68
+ }
69
+ export interface ChannelRuntimeStore {
70
+ claimEventReceipt(input: ClaimEventReceiptInput): Promise<ClaimEventReceiptResult>;
71
+ updateReceiptStatus(receiptId: string, status: ChannelEventReceiptRecord['status'], error?: {
72
+ code: string;
73
+ message: string;
74
+ }): Promise<void>;
75
+ upsertThread(input: UpsertThreadInput): Promise<ChannelThreadRecord>;
76
+ saveDecision(input: SaveDecisionInput): Promise<ChannelDecisionRecord>;
77
+ enqueueOutboxAction(input: EnqueueOutboxActionInput): Promise<EnqueueOutboxActionResult>;
78
+ claimPendingOutboxActions(limit: number, now?: Date): Promise<ChannelOutboxActionRecord[]>;
79
+ recordDeliveryAttempt(input: RecordDeliveryAttemptInput): Promise<ChannelDeliveryAttemptRecord>;
80
+ markOutboxSent(actionId: string, providerMessageId?: string): Promise<void>;
81
+ markOutboxRetry(input: MarkOutboxRetryInput): Promise<void>;
82
+ markOutboxDeadLetter(input: MarkOutboxDeadLetterInput): Promise<void>;
83
+ }
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,17 @@
1
+ export type ChannelTelemetryStage = 'ingest' | 'decision' | 'outbox' | 'dispatch';
2
+ export type ChannelTelemetryStatus = 'accepted' | 'duplicate' | 'processed' | 'failed' | 'sent' | 'retry' | 'dead_letter';
3
+ export interface ChannelTelemetryEvent {
4
+ stage: ChannelTelemetryStage;
5
+ status: ChannelTelemetryStatus;
6
+ workspaceId?: string;
7
+ providerKey?: string;
8
+ receiptId?: string;
9
+ actionId?: string;
10
+ traceId?: string;
11
+ latencyMs?: number;
12
+ attempt?: number;
13
+ metadata?: Record<string, string | number | boolean>;
14
+ }
15
+ export interface ChannelTelemetryEmitter {
16
+ record(event: ChannelTelemetryEvent): Promise<void> | void;
17
+ }
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,111 @@
1
+ export type ChannelProviderKey = 'messaging.slack' | 'messaging.github' | 'messaging.whatsapp.meta' | 'messaging.whatsapp.twilio' | (string & {});
2
+ export interface ChannelThreadRef {
3
+ externalThreadId: string;
4
+ externalChannelId?: string;
5
+ externalUserId?: string;
6
+ }
7
+ export interface InboundMessage {
8
+ text: string;
9
+ externalMessageId?: string;
10
+ }
11
+ export interface ChannelInboundEvent {
12
+ workspaceId: string;
13
+ providerKey: ChannelProviderKey;
14
+ externalEventId: string;
15
+ eventType: string;
16
+ occurredAt: Date;
17
+ signatureValid: boolean;
18
+ traceId?: string;
19
+ thread: ChannelThreadRef;
20
+ message?: InboundMessage;
21
+ metadata?: Record<string, string>;
22
+ rawPayload?: string;
23
+ }
24
+ export type ChannelRiskTier = 'low' | 'medium' | 'high' | 'blocked';
25
+ export type ChannelPolicyVerdict = 'autonomous' | 'assist' | 'blocked';
26
+ export interface ChannelPolicyDecision {
27
+ confidence: number;
28
+ riskTier: ChannelRiskTier;
29
+ verdict: ChannelPolicyVerdict;
30
+ reasons: string[];
31
+ responseText: string;
32
+ requiresApproval: boolean;
33
+ }
34
+ export interface ChannelEventReceiptRecord {
35
+ id: string;
36
+ workspaceId: string;
37
+ providerKey: string;
38
+ externalEventId: string;
39
+ eventType: string;
40
+ status: 'accepted' | 'processing' | 'processed' | 'duplicate' | 'failed' | 'rejected';
41
+ signatureValid: boolean;
42
+ payloadHash?: string;
43
+ traceId?: string;
44
+ firstSeenAt: Date;
45
+ lastSeenAt: Date;
46
+ processedAt?: Date;
47
+ errorCode?: string;
48
+ errorMessage?: string;
49
+ }
50
+ export interface ChannelThreadRecord {
51
+ id: string;
52
+ workspaceId: string;
53
+ providerKey: string;
54
+ externalThreadId: string;
55
+ externalChannelId?: string;
56
+ externalUserId?: string;
57
+ state: Record<string, unknown>;
58
+ lastProviderEventAt?: Date;
59
+ createdAt: Date;
60
+ updatedAt: Date;
61
+ }
62
+ export interface ChannelDecisionRecord {
63
+ id: string;
64
+ receiptId: string;
65
+ threadId: string;
66
+ policyMode: 'suggest' | 'assist' | 'autonomous';
67
+ riskTier: ChannelRiskTier;
68
+ confidence: number;
69
+ modelName: string;
70
+ promptVersion: string;
71
+ policyVersion: string;
72
+ toolTrace: Record<string, unknown>[];
73
+ actionPlan: Record<string, unknown>;
74
+ requiresApproval: boolean;
75
+ approvedBy?: string;
76
+ approvedAt?: Date;
77
+ createdAt: Date;
78
+ }
79
+ export interface ChannelOutboxActionRecord {
80
+ id: string;
81
+ workspaceId: string;
82
+ providerKey: string;
83
+ decisionId: string;
84
+ threadId: string;
85
+ actionType: string;
86
+ idempotencyKey: string;
87
+ target: Record<string, unknown>;
88
+ payload: Record<string, unknown>;
89
+ status: 'pending' | 'sending' | 'sent' | 'retryable' | 'failed' | 'dead_letter' | 'cancelled';
90
+ attemptCount: number;
91
+ nextAttemptAt: Date;
92
+ providerMessageId?: string;
93
+ lastErrorCode?: string;
94
+ lastErrorMessage?: string;
95
+ createdAt: Date;
96
+ updatedAt: Date;
97
+ sentAt?: Date;
98
+ }
99
+ export interface ChannelDeliveryAttemptRecord {
100
+ id: number;
101
+ actionId: string;
102
+ attempt: number;
103
+ responseStatus?: number;
104
+ responseBody?: string;
105
+ latencyMs?: number;
106
+ createdAt: Date;
107
+ }
108
+ export interface ChannelIngestResult {
109
+ status: 'accepted' | 'duplicate';
110
+ receiptId: string;
111
+ }
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,55 @@
1
+ import type { ChannelInboundEvent } from './types';
2
+ export interface MetaWhatsappWebhookPayload {
3
+ object?: string;
4
+ entry?: {
5
+ id?: string;
6
+ changes?: {
7
+ field?: string;
8
+ value?: {
9
+ metadata?: {
10
+ phone_number_id?: string;
11
+ display_phone_number?: string;
12
+ };
13
+ contacts?: {
14
+ wa_id?: string;
15
+ profile?: {
16
+ name?: string;
17
+ };
18
+ }[];
19
+ messages?: {
20
+ id?: string;
21
+ from?: string;
22
+ timestamp?: string;
23
+ type?: string;
24
+ text?: {
25
+ body?: string;
26
+ };
27
+ }[];
28
+ statuses?: {
29
+ id?: string;
30
+ recipient_id?: string;
31
+ status?: string;
32
+ timestamp?: string;
33
+ }[];
34
+ };
35
+ }[];
36
+ }[];
37
+ }
38
+ export interface MetaSignatureVerificationInput {
39
+ appSecret: string;
40
+ signatureHeader: string | null;
41
+ rawBody: string;
42
+ }
43
+ export interface MetaSignatureVerificationResult {
44
+ valid: boolean;
45
+ reason?: string;
46
+ }
47
+ export declare function verifyMetaSignature(input: MetaSignatureVerificationInput): MetaSignatureVerificationResult;
48
+ export declare function parseMetaWebhookPayload(rawBody: string): MetaWhatsappWebhookPayload;
49
+ export declare function normalizeMetaWhatsappInboundEvents(input: {
50
+ workspaceId: string;
51
+ payload: MetaWhatsappWebhookPayload;
52
+ signatureValid: boolean;
53
+ traceId?: string;
54
+ rawBody?: string;
55
+ }): ChannelInboundEvent[];