@vellumai/vellum-gateway 0.7.2 → 0.7.3
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/ARCHITECTURE.md +20 -21
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/__tests__/config-file-watcher.test.ts +1 -1
- package/src/__tests__/contact-prompt-submit.test.ts +349 -0
- package/src/__tests__/ipc-route-policy.test.ts +24 -0
- package/src/__tests__/nonbash-trust-rule-overrides.test.ts +50 -0
- package/src/__tests__/remote-feature-flag-sync.test.ts +16 -14
- package/src/__tests__/slack-display-name.test.ts +6 -2
- package/src/__tests__/slack-normalize.test.ts +36 -56
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +4 -2
- package/src/__tests__/telegram-webhook-manager.test.ts +8 -25
- package/src/__tests__/twilio-webhooks.test.ts +2 -6
- package/src/__tests__/upsert-verified-contact-channel.test.ts +173 -0
- package/src/auth/guardian-bootstrap.ts +49 -0
- package/src/auth/ipc-route-policy.ts +5 -0
- package/src/db/contact-store.ts +27 -1
- package/src/email/register-callback.test.ts +4 -4
- package/src/email/register-callback.ts +12 -16
- package/src/feature-flag-registry.json +27 -3
- package/src/handlers/handle-inbound.ts +12 -0
- package/src/http/routes/contact-prompt.ts +134 -23
- package/src/http/routes/contacts-control-plane-proxy.ts +34 -5
- package/src/http/routes/ipc-runtime-proxy.ts +18 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +22 -1
- package/src/http/routes/twilio-voice-webhook.ts +53 -0
- package/src/index.ts +4 -2
- package/src/ipc/velay-handlers.ts +31 -0
- package/src/remote-feature-flag-sync.ts +10 -8
- package/src/risk/command-registry/commands/assistant.ts +1 -0
- package/src/risk/skill-risk-classifier.ts +12 -3
- package/src/runtime/client.ts +25 -12
- package/src/slack/normalize.test.ts +3 -3
- package/src/slack/normalize.ts +6 -69
- package/src/slack/socket-mode.ts +1 -5
- package/src/telegram/webhook-manager.ts +9 -13
- package/src/velay/client.ts +27 -16
- package/src/verification/contact-helpers.ts +6 -3
|
@@ -116,13 +116,13 @@ function defaultCredentials(): Record<string, string> {
|
|
|
116
116
|
// Setup / teardown
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
118
118
|
const savedVellumPlatformUrl = process.env.VELLUM_PLATFORM_URL;
|
|
119
|
-
const
|
|
119
|
+
const savedAssistantCredential = process.env.ASSISTANT_API_KEY;
|
|
120
120
|
|
|
121
121
|
beforeEach(() => {
|
|
122
122
|
// Clear env vars that the production code falls back to, so tests remain
|
|
123
123
|
// deterministic unless they explicitly set them.
|
|
124
124
|
delete process.env.VELLUM_PLATFORM_URL;
|
|
125
|
-
delete process.env.
|
|
125
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
126
126
|
mkdirSync(protectedDir, { recursive: true });
|
|
127
127
|
// Write the test registry and point resolution at it
|
|
128
128
|
writeFileSync(testRegistryPath, JSON.stringify(TEST_REGISTRY, null, 2));
|
|
@@ -142,7 +142,7 @@ afterEach(() => {
|
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
144
|
restoreEnv("VELLUM_PLATFORM_URL", savedVellumPlatformUrl);
|
|
145
|
-
restoreEnv("
|
|
145
|
+
restoreEnv("ASSISTANT_API_KEY", savedAssistantCredential);
|
|
146
146
|
try {
|
|
147
147
|
rmSync(protectedDir, { recursive: true, force: true });
|
|
148
148
|
mkdirSync(protectedDir, { recursive: true });
|
|
@@ -195,7 +195,7 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
195
195
|
);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
test("skips sync when assistant_api_key is missing
|
|
198
|
+
test("skips sync when assistant_api_key is missing", async () => {
|
|
199
199
|
const creds = defaultCredentials();
|
|
200
200
|
delete creds["credential/vellum/assistant_api_key"];
|
|
201
201
|
|
|
@@ -208,12 +208,13 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
208
208
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
test("
|
|
212
|
-
fetchMock = mock(async () => Response.json({ flags: {} }));
|
|
213
|
-
process.env.PLATFORM_INTERNAL_API_KEY = "internal-key-123";
|
|
211
|
+
test("syncs when only platformUrl and assistantApiKey are present", async () => {
|
|
212
|
+
fetchMock = mock(async () => Response.json({ flags: { ff1: true } }));
|
|
214
213
|
|
|
215
|
-
const creds =
|
|
216
|
-
|
|
214
|
+
const creds = {
|
|
215
|
+
"credential/vellum/platform_base_url": "https://platform.example.com",
|
|
216
|
+
"credential/vellum/assistant_api_key": "test-api-key",
|
|
217
|
+
};
|
|
217
218
|
|
|
218
219
|
const sync = new RemoteFeatureFlagSync({
|
|
219
220
|
credentials: fakeCredentialCache(creds),
|
|
@@ -221,17 +222,15 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
221
222
|
await sync.start();
|
|
222
223
|
sync.stop();
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
// feature flag sync requires assistant_api_key (Api-Key auth).
|
|
226
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
225
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
227
226
|
});
|
|
228
227
|
|
|
229
|
-
test("
|
|
228
|
+
test("falls back to ASSISTANT_API_KEY env var when credential key is missing", async () => {
|
|
230
229
|
fetchMock = mock(async () => Response.json({ flags: { ff1: true } }));
|
|
230
|
+
process.env.ASSISTANT_API_KEY = "env-key";
|
|
231
231
|
|
|
232
232
|
const creds = {
|
|
233
233
|
"credential/vellum/platform_base_url": "https://platform.example.com",
|
|
234
|
-
"credential/vellum/assistant_api_key": "test-api-key",
|
|
235
234
|
};
|
|
236
235
|
|
|
237
236
|
const sync = new RemoteFeatureFlagSync({
|
|
@@ -241,6 +240,9 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
241
240
|
sync.stop();
|
|
242
241
|
|
|
243
242
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
243
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
244
|
+
const headers = init?.headers as Record<string, string>;
|
|
245
|
+
expect(headers.Authorization).toBe("Api-Key env-key");
|
|
244
246
|
});
|
|
245
247
|
|
|
246
248
|
test("fetches and caches flags on successful response", async () => {
|
|
@@ -259,7 +259,9 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
259
259
|
);
|
|
260
260
|
|
|
261
261
|
expect(result).not.toBeNull();
|
|
262
|
-
expect(result!.event.message.content).toBe(
|
|
262
|
+
expect(result!.event.message.content).toBe(
|
|
263
|
+
"@unknown-user @Leo please look",
|
|
264
|
+
);
|
|
263
265
|
expect(result!.event.message.content).not.toContain("<@ULEO>");
|
|
264
266
|
expect(result!.event.message.content).not.toContain("ULEO");
|
|
265
267
|
});
|
|
@@ -285,7 +287,9 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
285
287
|
);
|
|
286
288
|
|
|
287
289
|
expect(result).not.toBeNull();
|
|
288
|
-
expect(result!.event.message.content).toBe(
|
|
290
|
+
expect(result!.event.message.content).toBe(
|
|
291
|
+
"@unknown-user @unknown-user please look",
|
|
292
|
+
);
|
|
289
293
|
expect(result!.event.message.content).not.toContain("<@UFAIL>");
|
|
290
294
|
expect(result!.event.message.content).not.toContain("UFAIL");
|
|
291
295
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
stripBotMention,
|
|
4
3
|
normalizeSlackAppMention,
|
|
5
4
|
normalizeSlackChannelMessage,
|
|
6
5
|
normalizeSlackDirectMessage,
|
|
@@ -52,42 +51,6 @@ function makeEvent(
|
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
describe("stripBotMention", () => {
|
|
56
|
-
test("strips a single leading bot mention", () => {
|
|
57
|
-
expect(stripBotMention("<@U123BOT> hello world")).toBe("hello world");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("strips repeated leading bot mentions while preserving a following human mention", () => {
|
|
61
|
-
expect(
|
|
62
|
-
stripBotMention("<@U123BOT> <@U123BOT> <@U456OTHER> hello", "U123BOT"),
|
|
63
|
-
).toBe("<@U456OTHER> hello");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("fallback strips only the first leading mention", () => {
|
|
67
|
-
expect(stripBotMention("<@U123BOT> <@U456OTHER> hello")).toBe(
|
|
68
|
-
"<@U456OTHER> hello",
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("falls back to original text when stripping produces empty string", () => {
|
|
73
|
-
expect(stripBotMention("<@U123BOT>")).toBe("<@U123BOT>");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("falls back to original trimmed text when stripping produces whitespace only", () => {
|
|
77
|
-
expect(stripBotMention("<@U123BOT> ")).toBe("<@U123BOT>");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("returns text unchanged when no leading mention", () => {
|
|
81
|
-
expect(stripBotMention("hello world")).toBe("hello world");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("does not strip mid-text mentions", () => {
|
|
85
|
-
expect(stripBotMention("hello <@U123BOT> world")).toBe(
|
|
86
|
-
"hello <@U123BOT> world",
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
54
|
describe("normalizeSlackAppMention", () => {
|
|
92
55
|
test("normalizes app_mention event with sourceChannel 'slack'", async () => {
|
|
93
56
|
const config = makeConfig();
|
|
@@ -129,16 +92,23 @@ describe("normalizeSlackAppMention", () => {
|
|
|
129
92
|
expect(result!.event.message.externalMessageId).toBe("1700000000.000100");
|
|
130
93
|
});
|
|
131
94
|
|
|
132
|
-
test("
|
|
95
|
+
test("renders the bot's mention using the resolved label", async () => {
|
|
133
96
|
const config = makeConfig();
|
|
134
97
|
const event = makeEvent({ text: "<@U123BOT> hello world" });
|
|
135
|
-
const result = await normalizeSlackAppMention(
|
|
98
|
+
const result = await normalizeSlackAppMention(
|
|
99
|
+
event,
|
|
100
|
+
"evt-005",
|
|
101
|
+
config,
|
|
102
|
+
"U123BOT",
|
|
103
|
+
undefined,
|
|
104
|
+
{ userLabels: { U123BOT: "vex" } },
|
|
105
|
+
);
|
|
136
106
|
|
|
137
107
|
expect(result).not.toBeNull();
|
|
138
|
-
expect(result!.event.message.content).toBe("hello world");
|
|
108
|
+
expect(result!.event.message.content).toBe("@vex hello world");
|
|
139
109
|
});
|
|
140
110
|
|
|
141
|
-
test("
|
|
111
|
+
test("renders the bot's mention with the unknown-user fallback when unresolved", async () => {
|
|
142
112
|
const config = makeConfig();
|
|
143
113
|
const event = makeEvent({ text: "<@U123BOT>" });
|
|
144
114
|
const result = await normalizeSlackAppMention(event, "evt-006", config);
|
|
@@ -147,7 +117,7 @@ describe("normalizeSlackAppMention", () => {
|
|
|
147
117
|
expect(result!.event.message.content).toBe("@unknown-user");
|
|
148
118
|
});
|
|
149
119
|
|
|
150
|
-
test("renders
|
|
120
|
+
test("renders bot and human mentions side by side", async () => {
|
|
151
121
|
const config = makeConfig();
|
|
152
122
|
const event = makeEvent({ text: "<@UBOT> <@ULEO> can you check?" });
|
|
153
123
|
const result = await normalizeSlackAppMention(
|
|
@@ -156,11 +126,11 @@ describe("normalizeSlackAppMention", () => {
|
|
|
156
126
|
config,
|
|
157
127
|
"UBOT",
|
|
158
128
|
undefined,
|
|
159
|
-
{ userLabels: { ULEO: "leo" } },
|
|
129
|
+
{ userLabels: { UBOT: "vex", ULEO: "leo" } },
|
|
160
130
|
);
|
|
161
131
|
|
|
162
132
|
expect(result).not.toBeNull();
|
|
163
|
-
expect(result!.event.message.content).toBe("@leo can you check?");
|
|
133
|
+
expect(result!.event.message.content).toBe("@vex @leo can you check?");
|
|
164
134
|
});
|
|
165
135
|
|
|
166
136
|
test("renders unresolved user mentions with the unknown-user fallback", async () => {
|
|
@@ -174,7 +144,9 @@ describe("normalizeSlackAppMention", () => {
|
|
|
174
144
|
);
|
|
175
145
|
|
|
176
146
|
expect(result).not.toBeNull();
|
|
177
|
-
expect(result!.event.message.content).toBe(
|
|
147
|
+
expect(result!.event.message.content).toBe(
|
|
148
|
+
"@unknown-user @unknown-user can you check?",
|
|
149
|
+
);
|
|
178
150
|
});
|
|
179
151
|
|
|
180
152
|
test("thread_ts is preserved in return value", async () => {
|
|
@@ -295,7 +267,7 @@ describe("Slack inbound mention rendering", () => {
|
|
|
295
267
|
expect(result!.event.message.conversationExternalId).toBe("D_DIRECT1");
|
|
296
268
|
});
|
|
297
269
|
|
|
298
|
-
test("channel messages
|
|
270
|
+
test("channel messages render bot and human mentions inline", () => {
|
|
299
271
|
const config = makeConfig();
|
|
300
272
|
const event = makeChannelMessageEvent({
|
|
301
273
|
text: "<@UBOT> <@ULEO> hello",
|
|
@@ -306,16 +278,16 @@ describe("Slack inbound mention rendering", () => {
|
|
|
306
278
|
config,
|
|
307
279
|
"UBOT",
|
|
308
280
|
undefined,
|
|
309
|
-
{ userLabels: { ULEO: "leo" } },
|
|
281
|
+
{ userLabels: { UBOT: "vex", ULEO: "leo" } },
|
|
310
282
|
);
|
|
311
283
|
|
|
312
284
|
expect(result).not.toBeNull();
|
|
313
|
-
expect(result!.event.message.content).toBe("@leo hello");
|
|
285
|
+
expect(result!.event.message.content).toBe("@vex @leo hello");
|
|
314
286
|
expect(result!.event.actor.actorExternalId).toBe("U_USER123");
|
|
315
287
|
expect(result!.event.message.conversationExternalId).toBe("C_CHANNEL1");
|
|
316
288
|
});
|
|
317
289
|
|
|
318
|
-
test("channel messages
|
|
290
|
+
test("channel messages render with unknown-user fallback when bot label is missing", () => {
|
|
319
291
|
const config = makeConfig();
|
|
320
292
|
const event = makeChannelMessageEvent({
|
|
321
293
|
text: "<@UBOT> <@ULEO> hello",
|
|
@@ -330,10 +302,10 @@ describe("Slack inbound mention rendering", () => {
|
|
|
330
302
|
);
|
|
331
303
|
|
|
332
304
|
expect(result).not.toBeNull();
|
|
333
|
-
expect(result!.event.message.content).toBe("@leo hello");
|
|
305
|
+
expect(result!.event.message.content).toBe("@unknown-user @leo hello");
|
|
334
306
|
});
|
|
335
307
|
|
|
336
|
-
test("message edits render mentions and preserve edit metadata", () => {
|
|
308
|
+
test("message edits render bot and human mentions and preserve edit metadata", () => {
|
|
337
309
|
const config = makeConfig();
|
|
338
310
|
const event = makeMessageChangedEvent({
|
|
339
311
|
message: {
|
|
@@ -347,11 +319,11 @@ describe("Slack inbound mention rendering", () => {
|
|
|
347
319
|
"evt-edit-render",
|
|
348
320
|
config,
|
|
349
321
|
"UBOT",
|
|
350
|
-
{ userLabels: { ULEO: "leo" } },
|
|
322
|
+
{ userLabels: { UBOT: "vex", ULEO: "leo" } },
|
|
351
323
|
);
|
|
352
324
|
|
|
353
325
|
expect(result).not.toBeNull();
|
|
354
|
-
expect(result!.event.message.content).toBe("@leo edited");
|
|
326
|
+
expect(result!.event.message.content).toBe("@vex @leo edited");
|
|
355
327
|
expect(result!.event.message.isEdit).toBe(true);
|
|
356
328
|
expect(result!.event.message.externalMessageId).toBe("evt-edit-render");
|
|
357
329
|
expect(result!.event.source.messageId).toBe("1700000000.000100");
|
|
@@ -424,7 +396,7 @@ describe("normalizeSlackMessageEdit", () => {
|
|
|
424
396
|
expect(result).toBeNull();
|
|
425
397
|
});
|
|
426
398
|
|
|
427
|
-
test("
|
|
399
|
+
test("renders bot mention in edited text", () => {
|
|
428
400
|
const config = makeConfig();
|
|
429
401
|
const event = makeMessageChangedEvent({
|
|
430
402
|
message: {
|
|
@@ -433,10 +405,18 @@ describe("normalizeSlackMessageEdit", () => {
|
|
|
433
405
|
ts: "1700000000.000100",
|
|
434
406
|
},
|
|
435
407
|
});
|
|
436
|
-
const result = normalizeSlackMessageEdit(
|
|
408
|
+
const result = normalizeSlackMessageEdit(
|
|
409
|
+
event,
|
|
410
|
+
"evt-104",
|
|
411
|
+
config,
|
|
412
|
+
"U123BOT",
|
|
413
|
+
{
|
|
414
|
+
userLabels: { U123BOT: "vex" },
|
|
415
|
+
},
|
|
416
|
+
);
|
|
437
417
|
|
|
438
418
|
expect(result).not.toBeNull();
|
|
439
|
-
expect(result!.event.message.content).toBe("edited content");
|
|
419
|
+
expect(result!.event.message.content).toBe("@vex edited content");
|
|
440
420
|
});
|
|
441
421
|
|
|
442
422
|
test("sets actor.actorExternalId from edited message user", () => {
|
|
@@ -293,7 +293,7 @@ describe("SlackSocketModeClient thread tracking", () => {
|
|
|
293
293
|
expect(emitted).toHaveLength(2);
|
|
294
294
|
expect(emitted[0].event.source.updateId).toBe("Ev-race-mention");
|
|
295
295
|
expect(emitted[0].event.message.content).toBe(
|
|
296
|
-
"@Example User can you help here?",
|
|
296
|
+
"@Example User @Example User can you help here?",
|
|
297
297
|
);
|
|
298
298
|
expect(emitted[1].event.source.updateId).toBe("Ev-race-reply");
|
|
299
299
|
expect(emitted[1].event.message.content).toBe(
|
|
@@ -644,7 +644,9 @@ describe("SlackSocketModeClient thread tracking", () => {
|
|
|
644
644
|
await flushAsyncEventEmission();
|
|
645
645
|
|
|
646
646
|
expect(emitted).toHaveLength(1);
|
|
647
|
-
expect(emitted[0].event.message.content).toBe(
|
|
647
|
+
expect(emitted[0].event.message.content).toBe(
|
|
648
|
+
"@Example User @Leo please look",
|
|
649
|
+
);
|
|
648
650
|
expect(emitted[0].event.message.content).not.toContain("<@ULEO>");
|
|
649
651
|
expect(emitted[0].event.message.content).not.toContain("ULEO");
|
|
650
652
|
} finally {
|
|
@@ -20,6 +20,9 @@ const { reconcileTelegramWebhook } =
|
|
|
20
20
|
|
|
21
21
|
afterEach(() => {
|
|
22
22
|
fetchMock = mock(async () => new Response());
|
|
23
|
+
delete process.env.IS_CONTAINERIZED;
|
|
24
|
+
delete process.env.VELLUM_PLATFORM_URL;
|
|
25
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
function makeTelegramResponse(result: unknown) {
|
|
@@ -290,10 +293,9 @@ describe("reconcileTelegramWebhook", () => {
|
|
|
290
293
|
"https://platform.example.com/v1/gateway/callbacks/11111111-2222-4333-8444-555555555555/webhooks/telegram/",
|
|
291
294
|
);
|
|
292
295
|
expect((calls[2].body as any).secret_token).toBe("test-webhook-secret");
|
|
293
|
-
delete process.env.IS_CONTAINERIZED;
|
|
294
296
|
});
|
|
295
297
|
|
|
296
|
-
test("registers via credential cache for assistant ID", async () => {
|
|
298
|
+
test("registers via env assistant key and credential cache for assistant ID", async () => {
|
|
297
299
|
const calls: {
|
|
298
300
|
method: string;
|
|
299
301
|
body: unknown;
|
|
@@ -301,7 +303,7 @@ describe("reconcileTelegramWebhook", () => {
|
|
|
301
303
|
}[] = [];
|
|
302
304
|
process.env.IS_CONTAINERIZED = "true";
|
|
303
305
|
process.env.VELLUM_PLATFORM_URL = "https://env-platform.example.com";
|
|
304
|
-
process.env.
|
|
306
|
+
process.env.ASSISTANT_API_KEY = "env-key";
|
|
305
307
|
|
|
306
308
|
const caches = makeCaches({
|
|
307
309
|
ingressUrl: undefined,
|
|
@@ -359,21 +361,14 @@ describe("reconcileTelegramWebhook", () => {
|
|
|
359
361
|
callback_path: "webhooks/telegram",
|
|
360
362
|
type: "telegram",
|
|
361
363
|
});
|
|
362
|
-
|
|
363
|
-
expect(calls[0].headers?.Authorization).toBe(
|
|
364
|
-
"Bearer internal-key-from-env",
|
|
365
|
-
);
|
|
364
|
+
expect(calls[0].headers?.Authorization).toBe("Api-Key env-key");
|
|
366
365
|
expect(calls[2].method).toBe("setWebhook");
|
|
367
366
|
expect((calls[2].body as any).url).toBe(
|
|
368
367
|
"https://env-platform.example.com/v1/gateway/callbacks/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/webhooks/telegram/",
|
|
369
368
|
);
|
|
370
|
-
|
|
371
|
-
delete process.env.IS_CONTAINERIZED;
|
|
372
|
-
delete process.env.VELLUM_PLATFORM_URL;
|
|
373
|
-
delete process.env.PLATFORM_INTERNAL_API_KEY;
|
|
374
369
|
});
|
|
375
370
|
|
|
376
|
-
test("credential cache for assistant ID
|
|
371
|
+
test("credential cache for assistant ID, base URL, and auth key", async () => {
|
|
377
372
|
const calls: {
|
|
378
373
|
method: string;
|
|
379
374
|
body: unknown;
|
|
@@ -381,7 +376,6 @@ describe("reconcileTelegramWebhook", () => {
|
|
|
381
376
|
}[] = [];
|
|
382
377
|
process.env.IS_CONTAINERIZED = "true";
|
|
383
378
|
process.env.VELLUM_PLATFORM_URL = "https://env-platform.example.com";
|
|
384
|
-
process.env.PLATFORM_INTERNAL_API_KEY = "env-internal-key";
|
|
385
379
|
|
|
386
380
|
const caches = makeCaches({
|
|
387
381
|
ingressUrl: undefined,
|
|
@@ -435,28 +429,20 @@ describe("reconcileTelegramWebhook", () => {
|
|
|
435
429
|
expect(calls).toHaveLength(3);
|
|
436
430
|
expect(calls[0].method).toBe("registerCallbackRoute");
|
|
437
431
|
// platform_base_url: credential cache takes precedence over env var
|
|
438
|
-
// PLATFORM_INTERNAL_API_KEY: env var takes
|
|
439
|
-
// precedence, matching the daemon's resolvePlatformCallbackRegistrationContext().
|
|
440
432
|
expect(calls[0].body).toEqual({
|
|
441
433
|
assistant_id: "cache-assistant-id",
|
|
442
434
|
callback_path: "webhooks/telegram",
|
|
443
435
|
type: "telegram",
|
|
444
436
|
});
|
|
445
|
-
expect(calls[0].headers?.Authorization).toBe("
|
|
437
|
+
expect(calls[0].headers?.Authorization).toBe("Api-Key cache-api-key");
|
|
446
438
|
// Registration URL should use cache platform URL
|
|
447
439
|
expect((calls[2].body as any).url).toBe(
|
|
448
440
|
"https://cache-platform.example.com/v1/gateway/callbacks/cache-assistant-id/webhooks/telegram/",
|
|
449
441
|
);
|
|
450
|
-
|
|
451
|
-
delete process.env.IS_CONTAINERIZED;
|
|
452
|
-
delete process.env.VELLUM_PLATFORM_URL;
|
|
453
|
-
delete process.env.PLATFORM_INTERNAL_API_KEY;
|
|
454
442
|
});
|
|
455
443
|
|
|
456
444
|
test("skips registration when no platform URL is available from cache or env", async () => {
|
|
457
445
|
process.env.IS_CONTAINERIZED = "true";
|
|
458
|
-
process.env.PLATFORM_INTERNAL_API_KEY = "internal-key-from-env";
|
|
459
|
-
delete process.env.VELLUM_PLATFORM_URL;
|
|
460
446
|
|
|
461
447
|
const caches = makeCaches({
|
|
462
448
|
ingressUrl: undefined,
|
|
@@ -471,9 +457,6 @@ describe("reconcileTelegramWebhook", () => {
|
|
|
471
457
|
|
|
472
458
|
// No fetch calls should be made — registration is skipped
|
|
473
459
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
474
|
-
|
|
475
|
-
delete process.env.IS_CONTAINERIZED;
|
|
476
|
-
delete process.env.PLATFORM_INTERNAL_API_KEY;
|
|
477
460
|
});
|
|
478
461
|
|
|
479
462
|
test("calls setWebhook when current URL is empty", async () => {
|
|
@@ -150,11 +150,7 @@ function makeCaches(
|
|
|
150
150
|
ingressUrl?: string;
|
|
151
151
|
} = {},
|
|
152
152
|
) {
|
|
153
|
-
const {
|
|
154
|
-
authToken = AUTH_TOKEN,
|
|
155
|
-
ingressEnabled,
|
|
156
|
-
ingressUrl,
|
|
157
|
-
} = opts;
|
|
153
|
+
const { authToken = AUTH_TOKEN, ingressEnabled, ingressUrl } = opts;
|
|
158
154
|
const credentials = {
|
|
159
155
|
get: async (key: string, _opts?: { force?: boolean }) => {
|
|
160
156
|
if (key === credentialKey("twilio", "auth_token")) return authToken;
|
|
@@ -876,7 +872,7 @@ describe("Twilio webhook signature with canonical ingress base URL", () => {
|
|
|
876
872
|
});
|
|
877
873
|
|
|
878
874
|
describe("Twilio webhook force retry", () => {
|
|
879
|
-
test("refreshes
|
|
875
|
+
test("refreshes configured ingress URL before retrying signature validation", async () => {
|
|
880
876
|
fetchMock = mock(
|
|
881
877
|
async () =>
|
|
882
878
|
new Response('<?xml version="1.0" encoding="UTF-8"?><Response/>', {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for upsertVerifiedContactChannel: must not reactivate
|
|
3
|
+
* revoked or blocked channels.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect, beforeEach, mock } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import "./test-preload.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// DB mock — configurable per test
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
type ExistingRow = {
|
|
15
|
+
channelId: string;
|
|
16
|
+
contactId: string;
|
|
17
|
+
channelStatus: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let queryRows: ExistingRow[] = [];
|
|
21
|
+
const runCalls: { sql: string; params: unknown[] }[] = [];
|
|
22
|
+
|
|
23
|
+
mock.module("../db/assistant-db-proxy.js", () => ({
|
|
24
|
+
assistantDbQuery: async (_sql: string, _params: unknown[]) => queryRows,
|
|
25
|
+
assistantDbRun: async (sql: string, params: unknown[]) => {
|
|
26
|
+
runCalls.push({ sql, params });
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mock.module("../db/connection.js", () => ({
|
|
31
|
+
getGatewayDb: () => ({
|
|
32
|
+
update: () => ({ set: () => ({ where: () => ({ run: () => {} }) }) }),
|
|
33
|
+
insert: () => ({
|
|
34
|
+
values: () => ({ onConflictDoNothing: () => ({ run: () => {} }) }),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module("../db/schema.js", () => ({
|
|
40
|
+
contactChannels: "contactChannels",
|
|
41
|
+
contacts: "contacts",
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module("drizzle-orm", () => ({
|
|
45
|
+
eq: (col: unknown, val: unknown) => ({ col, val }),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
mock.module("../verification/identity.js", () => ({
|
|
49
|
+
canonicalizeInboundIdentity: (_channel: string, id: string) => id,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
mock.module("../ipc/socket-path.js", () => ({
|
|
53
|
+
resolveIpcSocketPath: () => ({ path: "/tmp/test.sock" }),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Import after mocks
|
|
57
|
+
const { upsertVerifiedContactChannel } = await import(
|
|
58
|
+
"../verification/contact-helpers.js"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
queryRows = [];
|
|
63
|
+
runCalls.length = 0;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Tests
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe("upsertVerifiedContactChannel — revoked/blocked guards", () => {
|
|
71
|
+
test("skips update when existing channel is revoked", async () => {
|
|
72
|
+
queryRows = [
|
|
73
|
+
{
|
|
74
|
+
channelId: "ch-1",
|
|
75
|
+
contactId: "co-1",
|
|
76
|
+
channelStatus: "revoked",
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
await upsertVerifiedContactChannel({
|
|
81
|
+
sourceChannel: "phone",
|
|
82
|
+
externalUserId: "+15550001111",
|
|
83
|
+
externalChatId: "+15550001111",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(runCalls.filter((c) => c.sql.includes("UPDATE"))).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("skips update when channel is blocked", async () => {
|
|
90
|
+
queryRows = [
|
|
91
|
+
{
|
|
92
|
+
channelId: "ch-2",
|
|
93
|
+
contactId: "co-2",
|
|
94
|
+
channelStatus: "blocked",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
await upsertVerifiedContactChannel({
|
|
99
|
+
sourceChannel: "phone",
|
|
100
|
+
externalUserId: "+15550001111",
|
|
101
|
+
externalChatId: "+15550001111",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(runCalls.filter((c) => c.sql.includes("UPDATE"))).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("skips update when a guardian's channel is revoked", async () => {
|
|
108
|
+
queryRows = [
|
|
109
|
+
{
|
|
110
|
+
channelId: "ch-3",
|
|
111
|
+
contactId: "co-3",
|
|
112
|
+
channelStatus: "revoked",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
await upsertVerifiedContactChannel({
|
|
117
|
+
sourceChannel: "phone",
|
|
118
|
+
externalUserId: "+15550001111",
|
|
119
|
+
externalChatId: "+15550001111",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(runCalls.filter((c) => c.sql.includes("UPDATE"))).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("updates an active channel belonging to a guardian contact", async () => {
|
|
126
|
+
queryRows = [
|
|
127
|
+
{
|
|
128
|
+
channelId: "ch-4",
|
|
129
|
+
contactId: "co-4",
|
|
130
|
+
channelStatus: "active",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
await upsertVerifiedContactChannel({
|
|
135
|
+
sourceChannel: "phone",
|
|
136
|
+
externalUserId: "+15550001111",
|
|
137
|
+
externalChatId: "+15550001111",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(runCalls.filter((c) => c.sql.includes("UPDATE"))).toHaveLength(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("updates an active channel belonging to a non-guardian contact", async () => {
|
|
144
|
+
queryRows = [
|
|
145
|
+
{
|
|
146
|
+
channelId: "ch-5",
|
|
147
|
+
contactId: "co-5",
|
|
148
|
+
channelStatus: "active",
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
await upsertVerifiedContactChannel({
|
|
153
|
+
sourceChannel: "phone",
|
|
154
|
+
externalUserId: "+15550001111",
|
|
155
|
+
externalChatId: "+15550001111",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(runCalls.filter((c) => c.sql.includes("UPDATE"))).toHaveLength(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("creates new contact + channel when no existing channel found", async () => {
|
|
162
|
+
queryRows = [];
|
|
163
|
+
|
|
164
|
+
await upsertVerifiedContactChannel({
|
|
165
|
+
sourceChannel: "phone",
|
|
166
|
+
externalUserId: "+15550009999",
|
|
167
|
+
externalChatId: "+15550009999",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const inserts = runCalls.filter((c) => c.sql.includes("INSERT"));
|
|
171
|
+
expect(inserts).toHaveLength(2);
|
|
172
|
+
});
|
|
173
|
+
});
|