@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.
- package/dist/channel/dispatcher.d.ts +37 -0
- package/dist/channel/dispatcher.js +130 -0
- package/dist/channel/dispatcher.test.d.ts +1 -0
- package/dist/channel/github.d.ts +47 -0
- package/dist/channel/github.js +58 -0
- package/dist/channel/github.test.d.ts +1 -0
- package/dist/channel/index.d.ts +14 -0
- package/dist/channel/index.js +1420 -0
- package/dist/channel/memory-store.d.ts +28 -0
- package/dist/channel/memory-store.js +223 -0
- package/dist/channel/policy.d.ts +19 -0
- package/dist/channel/policy.js +110 -0
- package/dist/channel/policy.test.d.ts +1 -0
- package/dist/channel/postgres-queries.d.ts +11 -0
- package/dist/channel/postgres-queries.js +222 -0
- package/dist/channel/postgres-schema.d.ts +1 -0
- package/dist/channel/postgres-schema.js +94 -0
- package/dist/channel/postgres-store.d.ts +21 -0
- package/dist/channel/postgres-store.js +498 -0
- package/dist/channel/postgres-store.test.d.ts +1 -0
- package/dist/channel/replay-fixtures.d.ts +8 -0
- package/dist/channel/replay-fixtures.js +31 -0
- package/dist/channel/replay.test.d.ts +1 -0
- package/dist/channel/service.d.ts +26 -0
- package/dist/channel/service.js +287 -0
- package/dist/channel/service.test.d.ts +1 -0
- package/dist/channel/slack.d.ts +42 -0
- package/dist/channel/slack.js +82 -0
- package/dist/channel/slack.test.d.ts +1 -0
- package/dist/channel/store.d.ts +83 -0
- package/dist/channel/store.js +1 -0
- package/dist/channel/telemetry.d.ts +17 -0
- package/dist/channel/telemetry.js +1 -0
- package/dist/channel/types.d.ts +111 -0
- package/dist/channel/types.js +1 -0
- package/dist/channel/whatsapp-meta.d.ts +55 -0
- package/dist/channel/whatsapp-meta.js +66 -0
- package/dist/channel/whatsapp-meta.test.d.ts +1 -0
- package/dist/channel/whatsapp-twilio.d.ts +20 -0
- package/dist/channel/whatsapp-twilio.js +61 -0
- package/dist/channel/whatsapp-twilio.test.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1443 -1
- package/dist/node/channel/dispatcher.js +129 -0
- package/dist/node/channel/github.js +57 -0
- package/dist/node/channel/index.js +1419 -0
- package/dist/node/channel/memory-store.js +222 -0
- package/dist/node/channel/policy.js +109 -0
- package/dist/node/channel/postgres-queries.js +221 -0
- package/dist/node/channel/postgres-schema.js +93 -0
- package/dist/node/channel/postgres-store.js +497 -0
- package/dist/node/channel/replay-fixtures.js +30 -0
- package/dist/node/channel/service.js +286 -0
- package/dist/node/channel/slack.js +81 -0
- package/dist/node/channel/store.js +0 -0
- package/dist/node/channel/telemetry.js +0 -0
- package/dist/node/channel/types.js +0 -0
- package/dist/node/channel/whatsapp-meta.js +65 -0
- package/dist/node/channel/whatsapp-twilio.js +60 -0
- package/dist/node/index.js +1443 -1
- package/dist/node/runtime.js +26 -1
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.health.test.d.ts +1 -0
- package/dist/runtime.js +26 -1
- 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[];
|