@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,286 @@
1
+ // src/channel/policy.ts
2
+ var DEFAULT_MESSAGING_POLICY_CONFIG = {
3
+ autoResolveMinConfidence: 0.85,
4
+ assistMinConfidence: 0.65,
5
+ blockedSignals: [
6
+ "ignore previous instructions",
7
+ "reveal secret",
8
+ "api key",
9
+ "password",
10
+ "token",
11
+ "drop table",
12
+ "delete repository"
13
+ ],
14
+ highRiskSignals: [
15
+ "refund",
16
+ "delete account",
17
+ "cancel subscription",
18
+ "permission",
19
+ "admin access",
20
+ "wire transfer",
21
+ "bank account"
22
+ ],
23
+ mediumRiskSignals: [
24
+ "urgent",
25
+ "legal",
26
+ "compliance",
27
+ "frustrated",
28
+ "escalate",
29
+ "outage"
30
+ ],
31
+ safeAckTemplate: "Thanks for your message. We received it and are preparing the next step."
32
+ };
33
+
34
+ class MessagingPolicyEngine {
35
+ config;
36
+ constructor(config) {
37
+ this.config = {
38
+ ...DEFAULT_MESSAGING_POLICY_CONFIG,
39
+ ...config ?? {}
40
+ };
41
+ }
42
+ evaluate(input) {
43
+ const text = (input.event.message?.text ?? "").toLowerCase();
44
+ if (containsAny(text, this.config.blockedSignals)) {
45
+ return {
46
+ confidence: 0.2,
47
+ riskTier: "blocked",
48
+ verdict: "blocked",
49
+ reasons: ["blocked_signal_detected"],
50
+ responseText: this.config.safeAckTemplate,
51
+ requiresApproval: true
52
+ };
53
+ }
54
+ if (containsAny(text, this.config.highRiskSignals)) {
55
+ return {
56
+ confidence: 0.55,
57
+ riskTier: "high",
58
+ verdict: "assist",
59
+ reasons: ["high_risk_topic_detected"],
60
+ responseText: this.config.safeAckTemplate,
61
+ requiresApproval: true
62
+ };
63
+ }
64
+ const mediumRiskDetected = containsAny(text, this.config.mediumRiskSignals);
65
+ const confidence = mediumRiskDetected ? 0.74 : 0.92;
66
+ const riskTier = mediumRiskDetected ? "medium" : "low";
67
+ if (confidence >= this.config.autoResolveMinConfidence && riskTier === "low") {
68
+ return {
69
+ confidence,
70
+ riskTier,
71
+ verdict: "autonomous",
72
+ reasons: ["low_risk_high_confidence"],
73
+ responseText: this.defaultResponseText(input.event),
74
+ requiresApproval: false
75
+ };
76
+ }
77
+ if (confidence >= this.config.assistMinConfidence) {
78
+ return {
79
+ confidence,
80
+ riskTier,
81
+ verdict: "assist",
82
+ reasons: ["needs_human_review"],
83
+ responseText: this.config.safeAckTemplate,
84
+ requiresApproval: true
85
+ };
86
+ }
87
+ return {
88
+ confidence,
89
+ riskTier: "blocked",
90
+ verdict: "blocked",
91
+ reasons: ["low_confidence"],
92
+ responseText: this.config.safeAckTemplate,
93
+ requiresApproval: true
94
+ };
95
+ }
96
+ defaultResponseText(event) {
97
+ if (!event.message?.text) {
98
+ return this.config.safeAckTemplate;
99
+ }
100
+ return `Acknowledged: ${event.message.text.slice(0, 240)}`;
101
+ }
102
+ }
103
+ function containsAny(text, candidates) {
104
+ return candidates.some((candidate) => text.includes(candidate));
105
+ }
106
+
107
+ // src/channel/service.ts
108
+ import { createHash, randomUUID } from "node:crypto";
109
+ class ChannelRuntimeService {
110
+ store;
111
+ policy;
112
+ asyncProcessing;
113
+ processInBackground;
114
+ modelName;
115
+ promptVersion;
116
+ policyVersion;
117
+ telemetry;
118
+ constructor(store, options = {}) {
119
+ this.store = store;
120
+ this.policy = options.policy ?? new MessagingPolicyEngine;
121
+ this.asyncProcessing = options.asyncProcessing ?? true;
122
+ this.processInBackground = options.processInBackground ?? ((task) => {
123
+ setTimeout(() => {
124
+ task();
125
+ }, 0);
126
+ });
127
+ this.modelName = options.modelName ?? "policy-heuristics-v1";
128
+ this.promptVersion = options.promptVersion ?? "channel-runtime.v1";
129
+ this.policyVersion = options.policyVersion ?? "messaging-policy.v1";
130
+ this.telemetry = options.telemetry;
131
+ }
132
+ async ingest(event) {
133
+ const startedAtMs = Date.now();
134
+ const claim = await this.store.claimEventReceipt({
135
+ workspaceId: event.workspaceId,
136
+ providerKey: event.providerKey,
137
+ externalEventId: event.externalEventId,
138
+ eventType: event.eventType,
139
+ signatureValid: event.signatureValid,
140
+ payloadHash: event.rawPayload ? sha256(event.rawPayload) : undefined,
141
+ traceId: event.traceId
142
+ });
143
+ if (claim.duplicate) {
144
+ this.telemetry?.record({
145
+ stage: "ingest",
146
+ status: "duplicate",
147
+ workspaceId: event.workspaceId,
148
+ providerKey: event.providerKey,
149
+ receiptId: claim.receiptId,
150
+ traceId: event.traceId,
151
+ latencyMs: Date.now() - startedAtMs
152
+ });
153
+ return {
154
+ status: "duplicate",
155
+ receiptId: claim.receiptId
156
+ };
157
+ }
158
+ this.telemetry?.record({
159
+ stage: "ingest",
160
+ status: "accepted",
161
+ workspaceId: event.workspaceId,
162
+ providerKey: event.providerKey,
163
+ receiptId: claim.receiptId,
164
+ traceId: event.traceId,
165
+ latencyMs: Date.now() - startedAtMs
166
+ });
167
+ const task = async () => {
168
+ await this.processAcceptedEvent(claim.receiptId, event);
169
+ };
170
+ if (this.asyncProcessing) {
171
+ this.processInBackground(task);
172
+ } else {
173
+ await task();
174
+ }
175
+ return {
176
+ status: "accepted",
177
+ receiptId: claim.receiptId
178
+ };
179
+ }
180
+ async processAcceptedEvent(receiptId, event) {
181
+ try {
182
+ await this.store.updateReceiptStatus(receiptId, "processing");
183
+ const thread = await this.store.upsertThread({
184
+ workspaceId: event.workspaceId,
185
+ providerKey: event.providerKey,
186
+ externalThreadId: event.thread.externalThreadId,
187
+ externalChannelId: event.thread.externalChannelId,
188
+ externalUserId: event.thread.externalUserId,
189
+ occurredAt: event.occurredAt
190
+ });
191
+ const policyDecision = this.policy.evaluate({ event });
192
+ this.telemetry?.record({
193
+ stage: "decision",
194
+ status: "processed",
195
+ workspaceId: event.workspaceId,
196
+ providerKey: event.providerKey,
197
+ receiptId,
198
+ traceId: event.traceId,
199
+ metadata: {
200
+ verdict: policyDecision.verdict,
201
+ riskTier: policyDecision.riskTier,
202
+ confidence: policyDecision.confidence
203
+ }
204
+ });
205
+ const decision = await this.store.saveDecision({
206
+ receiptId,
207
+ threadId: thread.id,
208
+ policyMode: policyDecision.verdict === "autonomous" ? "autonomous" : "assist",
209
+ riskTier: policyDecision.riskTier,
210
+ confidence: policyDecision.confidence,
211
+ modelName: this.modelName,
212
+ promptVersion: this.promptVersion,
213
+ policyVersion: this.policyVersion,
214
+ actionPlan: {
215
+ verdict: policyDecision.verdict,
216
+ reasons: policyDecision.reasons
217
+ },
218
+ requiresApproval: policyDecision.requiresApproval
219
+ });
220
+ if (policyDecision.verdict === "autonomous") {
221
+ await this.store.enqueueOutboxAction({
222
+ workspaceId: event.workspaceId,
223
+ providerKey: event.providerKey,
224
+ decisionId: decision.id,
225
+ threadId: thread.id,
226
+ actionType: "reply",
227
+ idempotencyKey: buildOutboxIdempotencyKey(event, policyDecision.responseText),
228
+ target: {
229
+ externalThreadId: event.thread.externalThreadId,
230
+ externalChannelId: event.thread.externalChannelId,
231
+ externalUserId: event.thread.externalUserId
232
+ },
233
+ payload: {
234
+ id: randomUUID(),
235
+ text: policyDecision.responseText
236
+ }
237
+ });
238
+ this.telemetry?.record({
239
+ stage: "outbox",
240
+ status: "accepted",
241
+ workspaceId: event.workspaceId,
242
+ providerKey: event.providerKey,
243
+ receiptId,
244
+ traceId: event.traceId,
245
+ metadata: {
246
+ actionType: "reply"
247
+ }
248
+ });
249
+ }
250
+ await this.store.updateReceiptStatus(receiptId, "processed");
251
+ this.telemetry?.record({
252
+ stage: "ingest",
253
+ status: "processed",
254
+ workspaceId: event.workspaceId,
255
+ providerKey: event.providerKey,
256
+ receiptId,
257
+ traceId: event.traceId
258
+ });
259
+ } catch (error) {
260
+ await this.store.updateReceiptStatus(receiptId, "failed", {
261
+ code: "PROCESSING_FAILED",
262
+ message: error instanceof Error ? error.message : String(error)
263
+ });
264
+ this.telemetry?.record({
265
+ stage: "ingest",
266
+ status: "failed",
267
+ workspaceId: event.workspaceId,
268
+ providerKey: event.providerKey,
269
+ receiptId,
270
+ traceId: event.traceId,
271
+ metadata: {
272
+ errorCode: "PROCESSING_FAILED"
273
+ }
274
+ });
275
+ }
276
+ }
277
+ }
278
+ function buildOutboxIdempotencyKey(event, responseText) {
279
+ return sha256(`${event.workspaceId}:${event.providerKey}:${event.externalEventId}:reply:${responseText}`);
280
+ }
281
+ function sha256(value) {
282
+ return createHash("sha256").update(value).digest("hex");
283
+ }
284
+ export {
285
+ ChannelRuntimeService
286
+ };
@@ -0,0 +1,81 @@
1
+ // src/channel/slack.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ function verifySlackSignature(input) {
4
+ if (!input.requestTimestamp || !input.requestSignature) {
5
+ return { valid: false, reason: "missing_signature_headers" };
6
+ }
7
+ const timestamp = Number.parseInt(input.requestTimestamp, 10);
8
+ if (!Number.isFinite(timestamp)) {
9
+ return { valid: false, reason: "invalid_timestamp" };
10
+ }
11
+ const nowMs = input.nowMs ?? Date.now();
12
+ const toleranceMs = (input.toleranceSeconds ?? 300) * 1000;
13
+ if (Math.abs(nowMs - timestamp * 1000) > toleranceMs) {
14
+ return { valid: false, reason: "timestamp_out_of_range" };
15
+ }
16
+ const base = `v0:${input.requestTimestamp}:${input.rawBody}`;
17
+ const expected = `v0=${createHmac("sha256", input.signingSecret).update(base).digest("hex")}`;
18
+ const receivedBuffer = Buffer.from(input.requestSignature, "utf8");
19
+ const expectedBuffer = Buffer.from(expected, "utf8");
20
+ if (receivedBuffer.length !== expectedBuffer.length) {
21
+ return { valid: false, reason: "signature_length_mismatch" };
22
+ }
23
+ const valid = timingSafeEqual(receivedBuffer, expectedBuffer);
24
+ return valid ? { valid: true } : { valid: false, reason: "signature_mismatch" };
25
+ }
26
+ function parseSlackWebhookPayload(rawBody) {
27
+ const parsed = JSON.parse(rawBody);
28
+ return parsed;
29
+ }
30
+ function isSlackUrlVerificationPayload(payload) {
31
+ return payload.type === "url_verification";
32
+ }
33
+ function normalizeSlackInboundEvent(input) {
34
+ if (input.payload.type !== "event_callback") {
35
+ return null;
36
+ }
37
+ const event = input.payload.event;
38
+ if (!event?.type) {
39
+ return null;
40
+ }
41
+ if (event.subtype === "bot_message" || event.bot_id) {
42
+ return null;
43
+ }
44
+ const externalEventId = input.payload.event_id ?? event.ts;
45
+ if (!externalEventId) {
46
+ return null;
47
+ }
48
+ const threadId = event.thread_ts ?? event.ts ?? externalEventId;
49
+ if (!threadId) {
50
+ return null;
51
+ }
52
+ return {
53
+ workspaceId: input.workspaceId,
54
+ providerKey: "messaging.slack",
55
+ externalEventId,
56
+ eventType: `slack.${event.type}`,
57
+ occurredAt: input.payload.event_time ? new Date(input.payload.event_time * 1000) : new Date,
58
+ signatureValid: input.signatureValid,
59
+ traceId: input.traceId,
60
+ rawPayload: input.rawBody,
61
+ thread: {
62
+ externalThreadId: threadId,
63
+ externalChannelId: event.channel,
64
+ externalUserId: event.user
65
+ },
66
+ message: event.text ? {
67
+ text: event.text,
68
+ externalMessageId: event.ts
69
+ } : undefined,
70
+ metadata: {
71
+ slackEventType: event.type,
72
+ slackTeamId: input.payload.team_id ?? input.workspaceId
73
+ }
74
+ };
75
+ }
76
+ export {
77
+ verifySlackSignature,
78
+ parseSlackWebhookPayload,
79
+ normalizeSlackInboundEvent,
80
+ isSlackUrlVerificationPayload
81
+ };
File without changes
File without changes
File without changes
@@ -0,0 +1,65 @@
1
+ // src/channel/whatsapp-meta.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ function verifyMetaSignature(input) {
4
+ if (!input.signatureHeader) {
5
+ return { valid: false, reason: "missing_signature" };
6
+ }
7
+ const expected = `sha256=${createHmac("sha256", input.appSecret).update(input.rawBody).digest("hex")}`;
8
+ const expectedBuffer = Buffer.from(expected, "utf8");
9
+ const providedBuffer = Buffer.from(input.signatureHeader, "utf8");
10
+ if (expectedBuffer.length !== providedBuffer.length) {
11
+ return { valid: false, reason: "signature_length_mismatch" };
12
+ }
13
+ return timingSafeEqual(expectedBuffer, providedBuffer) ? { valid: true } : { valid: false, reason: "signature_mismatch" };
14
+ }
15
+ function parseMetaWebhookPayload(rawBody) {
16
+ return JSON.parse(rawBody);
17
+ }
18
+ function normalizeMetaWhatsappInboundEvents(input) {
19
+ const events = [];
20
+ for (const entry of input.payload.entry ?? []) {
21
+ for (const change of entry.changes ?? []) {
22
+ const value = change.value;
23
+ if (!value)
24
+ continue;
25
+ const phoneNumberId = value.metadata?.phone_number_id;
26
+ for (const message of value.messages ?? []) {
27
+ const from = message.from;
28
+ const messageId = message.id;
29
+ const text = message.text?.body;
30
+ if (!from || !messageId || !text)
31
+ continue;
32
+ const occurredAt = message.timestamp ? new Date(Number(message.timestamp) * 1000) : new Date;
33
+ events.push({
34
+ workspaceId: input.workspaceId,
35
+ providerKey: "messaging.whatsapp.meta",
36
+ externalEventId: messageId,
37
+ eventType: "whatsapp.meta.message",
38
+ occurredAt,
39
+ signatureValid: input.signatureValid,
40
+ traceId: input.traceId,
41
+ rawPayload: input.rawBody,
42
+ thread: {
43
+ externalThreadId: from,
44
+ externalChannelId: phoneNumberId,
45
+ externalUserId: from
46
+ },
47
+ message: {
48
+ text,
49
+ externalMessageId: messageId
50
+ },
51
+ metadata: {
52
+ messageType: message.type ?? "text",
53
+ phoneNumberId: phoneNumberId ?? ""
54
+ }
55
+ });
56
+ }
57
+ }
58
+ }
59
+ return events;
60
+ }
61
+ export {
62
+ verifyMetaSignature,
63
+ parseMetaWebhookPayload,
64
+ normalizeMetaWhatsappInboundEvents
65
+ };
@@ -0,0 +1,60 @@
1
+ // src/channel/whatsapp-twilio.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ function verifyTwilioSignature(input) {
4
+ if (!input.signatureHeader) {
5
+ return { valid: false, reason: "missing_signature" };
6
+ }
7
+ const sortedKeys = Array.from(input.formBody.keys()).sort();
8
+ let payload = input.requestUrl;
9
+ for (const key of sortedKeys) {
10
+ const value = input.formBody.get(key) ?? "";
11
+ payload += `${key}${value}`;
12
+ }
13
+ const expected = createHmac("sha1", input.authToken).update(payload).digest("base64");
14
+ const expectedBuffer = Buffer.from(expected, "utf8");
15
+ const providedBuffer = Buffer.from(input.signatureHeader, "utf8");
16
+ if (expectedBuffer.length !== providedBuffer.length) {
17
+ return { valid: false, reason: "signature_length_mismatch" };
18
+ }
19
+ return timingSafeEqual(expectedBuffer, providedBuffer) ? { valid: true } : { valid: false, reason: "signature_mismatch" };
20
+ }
21
+ function parseTwilioFormPayload(rawBody) {
22
+ return new URLSearchParams(rawBody);
23
+ }
24
+ function normalizeTwilioWhatsappInboundEvent(input) {
25
+ const messageSid = input.formBody.get("MessageSid");
26
+ const from = input.formBody.get("From");
27
+ const to = input.formBody.get("To");
28
+ const body = input.formBody.get("Body");
29
+ if (!messageSid || !from || !body) {
30
+ return null;
31
+ }
32
+ return {
33
+ workspaceId: input.workspaceId,
34
+ providerKey: "messaging.whatsapp.twilio",
35
+ externalEventId: messageSid,
36
+ eventType: "whatsapp.twilio.message",
37
+ occurredAt: new Date,
38
+ signatureValid: input.signatureValid,
39
+ traceId: input.traceId,
40
+ rawPayload: input.rawBody,
41
+ thread: {
42
+ externalThreadId: from,
43
+ externalChannelId: to ?? undefined,
44
+ externalUserId: from
45
+ },
46
+ message: {
47
+ text: body,
48
+ externalMessageId: messageSid
49
+ },
50
+ metadata: {
51
+ accountSid: input.formBody.get("AccountSid") ?? "",
52
+ profileName: input.formBody.get("ProfileName") ?? ""
53
+ }
54
+ };
55
+ }
56
+ export {
57
+ verifyTwilioSignature,
58
+ parseTwilioFormPayload,
59
+ normalizeTwilioWhatsappInboundEvent
60
+ };