@vellumai/assistant 0.4.16 → 0.4.17

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 (58) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/call-controller.test.ts +1074 -751
  5. package/src/__tests__/call-routes-http.test.ts +329 -279
  6. package/src/__tests__/channel-approval-routes.test.ts +0 -11
  7. package/src/__tests__/channel-approvals.test.ts +227 -182
  8. package/src/__tests__/channel-guardian.test.ts +1 -0
  9. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  10. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  11. package/src/__tests__/conversation-routes.test.ts +71 -41
  12. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  13. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  14. package/src/__tests__/extract-email.test.ts +42 -0
  15. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  16. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  17. package/src/__tests__/gmail-integration.test.ts +48 -46
  18. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  19. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  20. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  21. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  22. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  23. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  24. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  25. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  26. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  27. package/src/__tests__/non-member-access-request.test.ts +291 -247
  28. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  29. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  30. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  31. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  32. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  33. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  34. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  35. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  39. package/src/__tests__/twilio-config.test.ts +49 -41
  40. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  41. package/src/__tests__/twilio-routes.test.ts +389 -280
  42. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  43. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  44. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
  45. package/src/config/env.ts +39 -29
  46. package/src/daemon/handlers/skills.ts +18 -10
  47. package/src/daemon/ipc-contract/messages.ts +1 -0
  48. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  49. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  50. package/src/daemon/session-agent-loop.ts +1 -1
  51. package/src/daemon/session-process.ts +1 -1
  52. package/src/daemon/session-surfaces.ts +42 -2
  53. package/src/runtime/auth/token-service.ts +74 -47
  54. package/src/sequence/reply-matcher.ts +10 -6
  55. package/src/skills/frontmatter.ts +9 -6
  56. package/src/tools/ui-surface/definitions.ts +2 -1
  57. package/src/util/platform.ts +0 -12
  58. package/docs/architecture/http-token-refresh.md +0 -274
@@ -5,49 +5,51 @@
5
5
  * granted access without guardian approval, and that invalid/expired/revoked
6
6
  * tokens produce the correct deterministic refusal messages.
7
7
  */
8
- import { mkdtempSync, rmSync } from 'node:fs';
9
- import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
8
+ import { mkdtempSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
11
 
12
- import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
12
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Test isolation: in-memory SQLite via temp directory
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
- const testDir = mkdtempSync(join(tmpdir(), 'inbound-invite-redemption-test-'));
18
+ const testDir = mkdtempSync(join(tmpdir(), "inbound-invite-redemption-test-"));
19
19
 
20
- mock.module('../util/platform.js', () => ({
20
+ mock.module("../util/platform.js", () => ({
21
21
  getRootDir: () => testDir,
22
22
  getDataDir: () => testDir,
23
- isMacOS: () => process.platform === 'darwin',
24
- isLinux: () => process.platform === 'linux',
25
- isWindows: () => process.platform === 'win32',
26
- getSocketPath: () => join(testDir, 'test.sock'),
27
- getPidPath: () => join(testDir, 'test.pid'),
28
- getDbPath: () => join(testDir, 'test.db'),
29
- getLogPath: () => join(testDir, 'test.log'),
23
+ isMacOS: () => process.platform === "darwin",
24
+ isLinux: () => process.platform === "linux",
25
+ isWindows: () => process.platform === "win32",
26
+ getSocketPath: () => join(testDir, "test.sock"),
27
+ getPidPath: () => join(testDir, "test.pid"),
28
+ getDbPath: () => join(testDir, "test.db"),
29
+ getLogPath: () => join(testDir, "test.log"),
30
30
  ensureDataDir: () => {},
31
- readHttpToken: () => 'test-bearer-token',
31
+ readHttpToken: () => "test-bearer-token",
32
32
  }));
33
33
 
34
- mock.module('../util/logger.js', () => ({
35
- getLogger: () => new Proxy({} as Record<string, unknown>, {
36
- get: () => () => {},
37
- }),
34
+ mock.module("../util/logger.js", () => ({
35
+ getLogger: () =>
36
+ new Proxy({} as Record<string, unknown>, {
37
+ get: () => () => {},
38
+ }),
38
39
  }));
39
40
 
40
- mock.module('../security/secret-ingress.js', () => ({
41
+ mock.module("../security/secret-ingress.js", () => ({
41
42
  checkIngressForSecrets: () => ({ blocked: false }),
42
43
  }));
43
44
 
44
- mock.module('../config/env.js', () => ({
45
- getGatewayInternalBaseUrl: () => 'http://127.0.0.1:7830',
45
+ mock.module("../config/env.js", () => ({
46
+ isHttpAuthDisabled: () => true,
47
+ getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
46
48
  }));
47
49
 
48
50
  // Mock the credential metadata store so the Telegram transport adapter
49
51
  // resolves without touching the filesystem.
50
- mock.module('../tools/credentials/metadata-store.js', () => ({
52
+ mock.module("../tools/credentials/metadata-store.js", () => ({
51
53
  getCredentialMetadata: () => undefined,
52
54
  upsertCredentialMetadata: () => {},
53
55
  deleteCredentialMetadata: () => {},
@@ -55,59 +57,69 @@ mock.module('../tools/credentials/metadata-store.js', () => ({
55
57
  }));
56
58
 
57
59
  const emitSignalCalls: Array<Record<string, unknown>> = [];
58
- mock.module('../notifications/emit-signal.js', () => ({
60
+ mock.module("../notifications/emit-signal.js", () => ({
59
61
  emitNotificationSignal: async (params: Record<string, unknown>) => {
60
62
  emitSignalCalls.push(params);
61
63
  return {
62
- signalId: 'mock-signal-id',
64
+ signalId: "mock-signal-id",
63
65
  deduplicated: false,
64
66
  dispatched: true,
65
- reason: 'mock',
67
+ reason: "mock",
66
68
  deliveryResults: [],
67
69
  };
68
70
  },
69
71
  }));
70
72
 
71
- const deliverReplyCalls: Array<{ url: string; payload: Record<string, unknown> }> = [];
72
- mock.module('../runtime/gateway-client.js', () => ({
73
- deliverChannelReply: async (url: string, payload: Record<string, unknown>) => {
73
+ const deliverReplyCalls: Array<{
74
+ url: string;
75
+ payload: Record<string, unknown>;
76
+ }> = [];
77
+ mock.module("../runtime/gateway-client.js", () => ({
78
+ deliverChannelReply: async (
79
+ url: string,
80
+ payload: Record<string, unknown>,
81
+ ) => {
74
82
  deliverReplyCalls.push({ url, payload });
75
83
  },
76
84
  }));
77
85
 
78
- mock.module('../runtime/approval-message-composer.js', () => ({
79
- composeApprovalMessage: () => 'mock approval message',
80
- composeApprovalMessageGenerative: async () => 'mock generative message',
86
+ mock.module("../runtime/approval-message-composer.js", () => ({
87
+ composeApprovalMessage: () => "mock approval message",
88
+ composeApprovalMessageGenerative: async () => "mock generative message",
81
89
  }));
82
90
 
83
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
84
- import { createInvite, revokeInvite } from '../memory/ingress-invite-store.js';
85
- import { findMember, upsertMember } from '../memory/ingress-member-store.js';
86
- import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
91
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
92
+ import { createInvite, revokeInvite } from "../memory/ingress-invite-store.js";
93
+ import { findMember, upsertMember } from "../memory/ingress-member-store.js";
94
+ import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
87
95
 
88
96
  initializeDb();
89
97
 
90
98
  afterAll(() => {
91
99
  resetDb();
92
- try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
100
+ try {
101
+ rmSync(testDir, { recursive: true });
102
+ } catch {
103
+ /* best effort */
104
+ }
93
105
  });
94
106
 
95
107
  // ---------------------------------------------------------------------------
96
108
  // Helpers
97
109
  // ---------------------------------------------------------------------------
98
110
 
99
- const TEST_BEARER_TOKEN = 'test-token';
111
+ const TEST_BEARER_TOKEN = "test-token";
100
112
  let msgCounter = 0;
101
113
 
102
114
  function resetState(): void {
103
115
  const db = getDb();
104
- db.run('DELETE FROM assistant_ingress_members');
105
- db.run('DELETE FROM assistant_ingress_invites');
106
- db.run('DELETE FROM channel_inbound_events');
107
- db.run('DELETE FROM conversations');
108
- db.run('DELETE FROM channel_guardian_approval_requests');
109
- db.run('DELETE FROM channel_guardian_bindings');
110
- db.run('DELETE FROM notification_events');
116
+ db.run("DELETE FROM assistant_ingress_members");
117
+ db.run("DELETE FROM assistant_ingress_invites");
118
+ db.run("DELETE FROM channel_inbound_events");
119
+ db.run("DELETE FROM conversations");
120
+ db.run("DELETE FROM channel_guardian_approval_requests");
121
+ db.run("DELETE FROM channel_guardian_bindings");
122
+ db.run("DELETE FROM notification_events");
111
123
  emitSignalCalls.length = 0;
112
124
  deliverReplyCalls.length = 0;
113
125
  msgCounter = 0;
@@ -116,26 +128,26 @@ function resetState(): void {
116
128
  function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
117
129
  msgCounter++;
118
130
  const body: Record<string, unknown> = {
119
- sourceChannel: 'telegram',
120
- interface: 'telegram',
121
- conversationExternalId: 'chat-invite-test',
131
+ sourceChannel: "telegram",
132
+ interface: "telegram",
133
+ conversationExternalId: "chat-invite-test",
122
134
  externalMessageId: `msg-invite-${Date.now()}-${msgCounter}`,
123
- content: '/start iv_sometoken',
124
- actorExternalId: 'user-invite-123',
125
- actorDisplayName: 'Invite User',
126
- actorUsername: 'invite_user',
127
- replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
135
+ content: "/start iv_sometoken",
136
+ actorExternalId: "user-invite-123",
137
+ actorDisplayName: "Invite User",
138
+ actorUsername: "invite_user",
139
+ replyCallbackUrl: "http://localhost:7830/deliver/telegram",
128
140
  sourceMetadata: {
129
- commandIntent: { type: 'start', payload: 'iv_sometoken' },
141
+ commandIntent: { type: "start", payload: "iv_sometoken" },
130
142
  },
131
143
  ...overrides,
132
144
  };
133
145
 
134
- return new Request('http://localhost:8080/channels/inbound', {
135
- method: 'POST',
146
+ return new Request("http://localhost:8080/channels/inbound", {
147
+ method: "POST",
136
148
  headers: {
137
- 'Content-Type': 'application/json',
138
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
149
+ "Content-Type": "application/json",
150
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
139
151
  },
140
152
  body: JSON.stringify(body),
141
153
  });
@@ -145,11 +157,14 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
145
157
  * Build a request with a specific invite token, using the structured
146
158
  * commandIntent that the gateway produces for `/start <payload>`.
147
159
  */
148
- function buildInviteRequest(rawToken: string, overrides: Record<string, unknown> = {}): Request {
160
+ function buildInviteRequest(
161
+ rawToken: string,
162
+ overrides: Record<string, unknown> = {},
163
+ ): Request {
149
164
  return buildInboundRequest({
150
165
  content: `/start iv_${rawToken}`,
151
166
  sourceMetadata: {
152
- commandIntent: { type: 'start', payload: `iv_${rawToken}` },
167
+ commandIntent: { type: "start", payload: `iv_${rawToken}` },
153
168
  },
154
169
  ...overrides,
155
170
  });
@@ -159,134 +174,155 @@ function buildInviteRequest(rawToken: string, overrides: Record<string, unknown>
159
174
  // Tests
160
175
  // ---------------------------------------------------------------------------
161
176
 
162
- describe('inbound invite redemption intercept', () => {
177
+ describe("inbound invite redemption intercept", () => {
163
178
  beforeEach(resetState);
164
179
 
165
- test('non-member with valid invite token becomes active member without guardian approval', async () => {
166
- const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
180
+ test("non-member with valid invite token becomes active member without guardian approval", async () => {
181
+ const { rawToken } = createInvite({
182
+ sourceChannel: "telegram",
183
+ maxUses: 5,
184
+ });
167
185
 
168
186
  const req = buildInviteRequest(rawToken);
169
187
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
170
- const json = await resp.json() as Record<string, unknown>;
188
+ const json = (await resp.json()) as Record<string, unknown>;
171
189
 
172
190
  expect(json.accepted).toBe(true);
173
- expect(json.inviteRedemption).toBe('redeemed');
191
+ expect(json.inviteRedemption).toBe("redeemed");
174
192
  expect(json.memberId).toEqual(expect.any(String));
175
193
  expect(json.denied).toBeUndefined();
176
194
 
177
195
  // Verify the user is now an active member
178
196
  const member = findMember({
179
- assistantId: 'self',
180
- sourceChannel: 'telegram',
181
- externalUserId: 'user-invite-123',
197
+ assistantId: "self",
198
+ sourceChannel: "telegram",
199
+ externalUserId: "user-invite-123",
182
200
  });
183
201
  expect(member).not.toBeNull();
184
- expect(member!.status).toBe('active');
202
+ expect(member!.status).toBe("active");
185
203
 
186
204
  // Verify a welcome reply was delivered
187
205
  expect(deliverReplyCalls.length).toBe(1);
188
- const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text;
189
- expect(replyText).toContain("Welcome! You've been granted access via invite link.");
206
+ const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>)
207
+ .text;
208
+ expect(replyText).toContain(
209
+ "Welcome! You've been granted access via invite link.",
210
+ );
190
211
  });
191
212
 
192
- test('non-member with invalid token gets refusal text', async () => {
193
- const req = buildInviteRequest('completely-bogus-token-xyz');
213
+ test("non-member with invalid token gets refusal text", async () => {
214
+ const req = buildInviteRequest("completely-bogus-token-xyz");
194
215
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
195
- const json = await resp.json() as Record<string, unknown>;
216
+ const json = (await resp.json()) as Record<string, unknown>;
196
217
 
197
218
  expect(json.accepted).toBe(true);
198
219
  expect(json.denied).toBe(true);
199
- expect(json.inviteRedemption).toBe('invalid_token');
220
+ expect(json.inviteRedemption).toBe("invalid_token");
200
221
 
201
222
  // Verify refusal reply was delivered
202
223
  expect(deliverReplyCalls.length).toBe(1);
203
- const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text;
204
- expect(replyText).toContain('no longer valid');
224
+ const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>)
225
+ .text;
226
+ expect(replyText).toContain("no longer valid");
205
227
 
206
228
  // Verify the user was NOT made a member
207
229
  const member = findMember({
208
- assistantId: 'self',
209
- sourceChannel: 'telegram',
210
- externalUserId: 'user-invite-123',
230
+ assistantId: "self",
231
+ sourceChannel: "telegram",
232
+ externalUserId: "user-invite-123",
211
233
  });
212
234
  expect(member).toBeNull();
213
235
  });
214
236
 
215
- test('non-member with expired token gets appropriate message', async () => {
237
+ test("non-member with expired token gets appropriate message", async () => {
216
238
  const { rawToken } = createInvite({
217
- sourceChannel: 'telegram',
239
+ sourceChannel: "telegram",
218
240
  maxUses: 1,
219
241
  expiresInMs: -1, // already expired
220
242
  });
221
243
 
222
244
  const req = buildInviteRequest(rawToken);
223
245
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
224
- const json = await resp.json() as Record<string, unknown>;
246
+ const json = (await resp.json()) as Record<string, unknown>;
225
247
 
226
248
  expect(json.accepted).toBe(true);
227
249
  expect(json.denied).toBe(true);
228
- expect(json.inviteRedemption).toBe('expired');
250
+ expect(json.inviteRedemption).toBe("expired");
229
251
 
230
252
  expect(deliverReplyCalls.length).toBe(1);
231
- const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text;
232
- expect(replyText).toContain('no longer valid');
253
+ const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>)
254
+ .text;
255
+ expect(replyText).toContain("no longer valid");
233
256
  });
234
257
 
235
- test('non-member with revoked token gets refusal text', async () => {
258
+ test("non-member with revoked token gets refusal text", async () => {
236
259
  const { rawToken, invite } = createInvite({
237
- sourceChannel: 'telegram',
260
+ sourceChannel: "telegram",
238
261
  maxUses: 5,
239
262
  });
240
263
  revokeInvite(invite.id);
241
264
 
242
265
  const req = buildInviteRequest(rawToken);
243
266
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
244
- const json = await resp.json() as Record<string, unknown>;
267
+ const json = (await resp.json()) as Record<string, unknown>;
245
268
 
246
269
  expect(json.accepted).toBe(true);
247
270
  expect(json.denied).toBe(true);
248
- expect(json.inviteRedemption).toBe('revoked');
271
+ expect(json.inviteRedemption).toBe("revoked");
249
272
 
250
273
  expect(deliverReplyCalls.length).toBe(1);
251
- const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text;
252
- expect(replyText).toContain('no longer valid');
274
+ const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>)
275
+ .text;
276
+ expect(replyText).toContain("no longer valid");
253
277
  });
254
278
 
255
- test('existing /start gv_<token> guardian bootstrap flow is unaffected', async () => {
279
+ test("existing /start gv_<token> guardian bootstrap flow is unaffected", async () => {
256
280
  // Send a /start gv_ command — should not be intercepted by the invite flow.
257
281
  // Without a valid bootstrap session, it should be denied at the ACL gate.
258
282
  const req = buildInboundRequest({
259
- content: '/start gv_some_bootstrap_token',
283
+ content: "/start gv_some_bootstrap_token",
260
284
  sourceMetadata: {
261
- commandIntent: { type: 'start', payload: 'gv_some_bootstrap_token' },
285
+ commandIntent: { type: "start", payload: "gv_some_bootstrap_token" },
262
286
  },
263
287
  });
264
288
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
265
- const json = await resp.json() as Record<string, unknown>;
289
+ const json = (await resp.json()) as Record<string, unknown>;
266
290
 
267
291
  // Should be denied as a non-member (bootstrap token is invalid/no session)
268
292
  expect(json.denied).toBe(true);
269
- expect(json.reason).toBe('not_a_member');
293
+ expect(json.reason).toBe("not_a_member");
270
294
  // Should NOT have invite redemption fields
271
295
  expect(json.inviteRedemption).toBeUndefined();
272
296
  });
273
297
 
274
- test('duplicate Telegram webhook deliveries do not double-redeem', async () => {
275
- const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
298
+ test("duplicate Telegram webhook deliveries do not double-redeem", async () => {
299
+ const { rawToken } = createInvite({
300
+ sourceChannel: "telegram",
301
+ maxUses: 5,
302
+ });
276
303
 
277
304
  const sharedMessageId = `msg-dedup-${Date.now()}`;
278
- const makeReq = () => buildInviteRequest(rawToken, {
279
- externalMessageId: sharedMessageId,
280
- });
305
+ const makeReq = () =>
306
+ buildInviteRequest(rawToken, {
307
+ externalMessageId: sharedMessageId,
308
+ });
281
309
 
282
310
  // First delivery
283
- const resp1 = await handleChannelInbound(makeReq(), undefined, TEST_BEARER_TOKEN);
284
- const json1 = await resp1.json() as Record<string, unknown>;
285
- expect(json1.inviteRedemption).toBe('redeemed');
311
+ const resp1 = await handleChannelInbound(
312
+ makeReq(),
313
+ undefined,
314
+ TEST_BEARER_TOKEN,
315
+ );
316
+ const json1 = (await resp1.json()) as Record<string, unknown>;
317
+ expect(json1.inviteRedemption).toBe("redeemed");
286
318
 
287
319
  // Second delivery (duplicate webhook)
288
- const resp2 = await handleChannelInbound(makeReq(), undefined, TEST_BEARER_TOKEN);
289
- const json2 = await resp2.json() as Record<string, unknown>;
320
+ const resp2 = await handleChannelInbound(
321
+ makeReq(),
322
+ undefined,
323
+ TEST_BEARER_TOKEN,
324
+ );
325
+ const json2 = (await resp2.json()) as Record<string, unknown>;
290
326
  // Dedup kicks in — the message is treated as a duplicate and no second
291
327
  // redemption attempt occurs.
292
328
  expect(json2.duplicate).toBe(true);
@@ -295,26 +331,26 @@ describe('inbound invite redemption intercept', () => {
295
331
  expect(deliverReplyCalls.length).toBe(1);
296
332
  });
297
333
 
298
- test('existing active member sending normal message is unaffected', async () => {
334
+ test("existing active member sending normal message is unaffected", async () => {
299
335
  // Pre-create an active member
300
336
  upsertMember({
301
- assistantId: 'self',
302
- sourceChannel: 'telegram',
303
- externalUserId: 'user-active-member',
304
- externalChatId: 'chat-active',
305
- status: 'active',
306
- policy: 'allow',
337
+ assistantId: "self",
338
+ sourceChannel: "telegram",
339
+ externalUserId: "user-active-member",
340
+ externalChatId: "chat-active",
341
+ status: "active",
342
+ policy: "allow",
307
343
  });
308
344
 
309
345
  // Active member sends a normal message (no invite token)
310
346
  const req = buildInboundRequest({
311
- content: 'Hello, just a normal message!',
312
- actorExternalId: 'user-active-member',
313
- conversationExternalId: 'chat-active',
347
+ content: "Hello, just a normal message!",
348
+ actorExternalId: "user-active-member",
349
+ conversationExternalId: "chat-active",
314
350
  sourceMetadata: {},
315
351
  });
316
352
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
317
- const json = await resp.json() as Record<string, unknown>;
353
+ const json = (await resp.json()) as Record<string, unknown>;
318
354
 
319
355
  // Should be accepted normally, not denied, not invite-redeemed
320
356
  expect(json.accepted).toBe(true);
@@ -322,41 +358,45 @@ describe('inbound invite redemption intercept', () => {
322
358
  expect(json.inviteRedemption).toBeUndefined();
323
359
  });
324
360
 
325
- test('channel mismatch returns appropriate message', async () => {
361
+ test("channel mismatch returns appropriate message", async () => {
326
362
  // Create an invite for SMS, but try to redeem via Telegram
327
- const { rawToken } = createInvite({ sourceChannel: 'sms', maxUses: 5 });
363
+ const { rawToken } = createInvite({ sourceChannel: "sms", maxUses: 5 });
328
364
 
329
365
  const req = buildInviteRequest(rawToken);
330
366
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
331
- const json = await resp.json() as Record<string, unknown>;
367
+ const json = (await resp.json()) as Record<string, unknown>;
332
368
 
333
369
  expect(json.accepted).toBe(true);
334
370
  expect(json.denied).toBe(true);
335
- expect(json.inviteRedemption).toBe('channel_mismatch');
371
+ expect(json.inviteRedemption).toBe("channel_mismatch");
336
372
 
337
373
  expect(deliverReplyCalls.length).toBe(1);
338
- const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text;
339
- expect(replyText).toContain('not valid for this channel');
374
+ const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>)
375
+ .text;
376
+ expect(replyText).toContain("not valid for this channel");
340
377
  });
341
378
 
342
- test('already-active member with invite token gets acknowledgement', async () => {
343
- const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
379
+ test("already-active member with invite token gets acknowledgement", async () => {
380
+ const { rawToken } = createInvite({
381
+ sourceChannel: "telegram",
382
+ maxUses: 5,
383
+ });
344
384
 
345
385
  // Pre-create an active member that will click the invite link
346
386
  upsertMember({
347
- assistantId: 'self',
348
- sourceChannel: 'telegram',
349
- externalUserId: 'user-already-active',
350
- externalChatId: 'chat-invite-test',
351
- status: 'active',
352
- policy: 'allow',
387
+ assistantId: "self",
388
+ sourceChannel: "telegram",
389
+ externalUserId: "user-already-active",
390
+ externalChatId: "chat-invite-test",
391
+ status: "active",
392
+ policy: "allow",
353
393
  });
354
394
 
355
395
  const req = buildInviteRequest(rawToken, {
356
- actorExternalId: 'user-already-active',
396
+ actorExternalId: "user-already-active",
357
397
  });
358
398
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
359
- const json = await resp.json() as Record<string, unknown>;
399
+ const json = (await resp.json()) as Record<string, unknown>;
360
400
 
361
401
  // Active members pass through the ACL gate, so the invite intercept
362
402
  // does not fire. The message proceeds to normal processing.
@@ -364,36 +404,39 @@ describe('inbound invite redemption intercept', () => {
364
404
  expect(json.denied).toBeUndefined();
365
405
  });
366
406
 
367
- test('reactivation via invite preserves existing guardian-managed member display name', async () => {
368
- const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
407
+ test("reactivation via invite preserves existing guardian-managed member display name", async () => {
408
+ const { rawToken } = createInvite({
409
+ sourceChannel: "telegram",
410
+ maxUses: 5,
411
+ });
369
412
 
370
413
  upsertMember({
371
- assistantId: 'self',
372
- sourceChannel: 'telegram',
373
- externalUserId: 'user-invite-123',
374
- externalChatId: 'chat-invite-test',
375
- status: 'revoked',
376
- policy: 'allow',
377
- displayName: 'Jeff',
414
+ assistantId: "self",
415
+ sourceChannel: "telegram",
416
+ externalUserId: "user-invite-123",
417
+ externalChatId: "chat-invite-test",
418
+ status: "revoked",
419
+ policy: "allow",
420
+ displayName: "Jeff",
378
421
  });
379
422
 
380
423
  const req = buildInviteRequest(rawToken, {
381
- actorDisplayName: 'Noa Flaherty',
424
+ actorDisplayName: "Noa Flaherty",
382
425
  });
383
426
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
384
- const json = await resp.json() as Record<string, unknown>;
427
+ const json = (await resp.json()) as Record<string, unknown>;
385
428
 
386
429
  expect(json.accepted).toBe(true);
387
- expect(json.inviteRedemption).toBe('redeemed');
430
+ expect(json.inviteRedemption).toBe("redeemed");
388
431
 
389
432
  const member = findMember({
390
- assistantId: 'self',
391
- sourceChannel: 'telegram',
392
- externalUserId: 'user-invite-123',
393
- externalChatId: 'chat-invite-test',
433
+ assistantId: "self",
434
+ sourceChannel: "telegram",
435
+ externalUserId: "user-invite-123",
436
+ externalChatId: "chat-invite-test",
394
437
  });
395
438
  expect(member).not.toBeNull();
396
- expect(member!.status).toBe('active');
397
- expect(member!.displayName).toBe('Jeff');
439
+ expect(member!.status).toBe("active");
440
+ expect(member!.displayName).toBe("Jeff");
398
441
  });
399
442
  });