@vellumai/assistant 0.4.15 → 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 (73) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/approval-routes-http.test.ts +383 -254
  5. package/src/__tests__/call-controller.test.ts +1074 -751
  6. package/src/__tests__/call-routes-http.test.ts +329 -279
  7. package/src/__tests__/channel-approval-routes.test.ts +2 -13
  8. package/src/__tests__/channel-approvals.test.ts +227 -182
  9. package/src/__tests__/channel-guardian.test.ts +1 -0
  10. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  11. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  12. package/src/__tests__/conversation-routes.test.ts +71 -41
  13. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  15. package/src/__tests__/extract-email.test.ts +42 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  17. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  18. package/src/__tests__/gmail-integration.test.ts +48 -46
  19. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  20. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  21. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  22. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  23. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  24. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  25. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  26. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  27. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  28. package/src/__tests__/non-member-access-request.test.ts +291 -247
  29. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  30. package/src/__tests__/pairing-concurrent.test.ts +78 -0
  31. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  32. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  33. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  34. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  35. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  36. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  37. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  38. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  39. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  40. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  41. package/src/__tests__/twilio-config.test.ts +49 -41
  42. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  43. package/src/__tests__/twilio-routes.test.ts +389 -280
  44. package/src/calls/call-controller.ts +1 -1
  45. package/src/calls/guardian-action-sweep.ts +6 -6
  46. package/src/calls/twilio-routes.ts +2 -4
  47. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  48. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  49. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
  50. package/src/config/env.ts +39 -29
  51. package/src/daemon/handlers/config-inbox.ts +5 -5
  52. package/src/daemon/handlers/skills.ts +18 -10
  53. package/src/daemon/ipc-contract/messages.ts +1 -0
  54. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  55. package/src/daemon/pairing-store.ts +15 -2
  56. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  57. package/src/daemon/session-agent-loop.ts +1 -1
  58. package/src/daemon/session-process.ts +1 -1
  59. package/src/daemon/session-slash.ts +4 -4
  60. package/src/daemon/session-surfaces.ts +42 -2
  61. package/src/runtime/auth/token-service.ts +95 -45
  62. package/src/runtime/channel-retry-sweep.ts +2 -2
  63. package/src/runtime/http-server.ts +8 -7
  64. package/src/runtime/http-types.ts +1 -1
  65. package/src/runtime/routes/conversation-routes.ts +1 -1
  66. package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
  67. package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
  68. package/src/runtime/routes/pairing-routes.ts +4 -1
  69. package/src/sequence/reply-matcher.ts +14 -4
  70. package/src/skills/frontmatter.ts +9 -6
  71. package/src/tools/ui-surface/definitions.ts +3 -1
  72. package/src/util/platform.ts +0 -12
  73. package/docs/architecture/http-token-refresh.md +0 -274
@@ -7,53 +7,55 @@
7
7
  * 3. Create a guardian approval request for the access request
8
8
  * 4. Deduplicate: don't create duplicate requests for repeated messages
9
9
  */
10
- import { mkdtempSync, rmSync } from 'node:fs';
11
- import { tmpdir } from 'node:os';
12
- import { join } from 'node:path';
10
+ import { mkdtempSync, rmSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
13
 
14
- import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
14
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
15
15
 
16
16
  // ---------------------------------------------------------------------------
17
17
  // Test isolation: in-memory SQLite via temp directory
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- const testDir = mkdtempSync(join(tmpdir(), 'non-member-access-request-test-'));
20
+ const testDir = mkdtempSync(join(tmpdir(), "non-member-access-request-test-"));
21
21
 
22
- mock.module('../util/platform.js', () => ({
22
+ mock.module("../util/platform.js", () => ({
23
23
  getRootDir: () => testDir,
24
24
  getDataDir: () => testDir,
25
- isMacOS: () => process.platform === 'darwin',
26
- isLinux: () => process.platform === 'linux',
27
- isWindows: () => process.platform === 'win32',
28
- getSocketPath: () => join(testDir, 'test.sock'),
29
- getPidPath: () => join(testDir, 'test.pid'),
30
- getDbPath: () => join(testDir, 'test.db'),
31
- getLogPath: () => join(testDir, 'test.log'),
25
+ isMacOS: () => process.platform === "darwin",
26
+ isLinux: () => process.platform === "linux",
27
+ isWindows: () => process.platform === "win32",
28
+ getSocketPath: () => join(testDir, "test.sock"),
29
+ getPidPath: () => join(testDir, "test.pid"),
30
+ getDbPath: () => join(testDir, "test.db"),
31
+ getLogPath: () => join(testDir, "test.log"),
32
32
  ensureDataDir: () => {},
33
- readHttpToken: () => 'test-bearer-token',
33
+ readHttpToken: () => "test-bearer-token",
34
34
  }));
35
35
 
36
- mock.module('../util/logger.js', () => ({
37
- getLogger: () => new Proxy({} as Record<string, unknown>, {
38
- get: () => () => {},
39
- }),
36
+ mock.module("../util/logger.js", () => ({
37
+ getLogger: () =>
38
+ new Proxy({} as Record<string, unknown>, {
39
+ get: () => () => {},
40
+ }),
40
41
  }));
41
42
 
42
43
  // Mock security check to always pass
43
- mock.module('../security/secret-ingress.js', () => ({
44
+ mock.module("../security/secret-ingress.js", () => ({
44
45
  checkIngressForSecrets: () => ({ blocked: false }),
45
46
  }));
46
47
 
47
48
  // Mock ingress member store: findMember always returns null (non-member),
48
49
  // updateLastSeen is a no-op.
49
- mock.module('../memory/ingress-member-store.js', () => ({
50
+ mock.module("../memory/ingress-member-store.js", () => ({
50
51
  findMember: () => null,
51
52
  updateLastSeen: () => {},
52
53
  upsertMember: () => {},
53
54
  }));
54
55
 
55
- mock.module('../config/env.js', () => ({
56
- getGatewayInternalBaseUrl: () => 'http://127.0.0.1:7830',
56
+ mock.module("../config/env.js", () => ({
57
+ isHttpAuthDisabled: () => true,
58
+ getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
57
59
  }));
58
60
 
59
61
  // Track emitNotificationSignal calls
@@ -65,13 +67,13 @@ let mockEmitResult: {
65
67
  reason: string;
66
68
  deliveryResults: Array<Record<string, unknown>>;
67
69
  } = {
68
- signalId: 'mock-signal-id',
70
+ signalId: "mock-signal-id",
69
71
  deduplicated: false,
70
72
  dispatched: true,
71
- reason: 'mock',
73
+ reason: "mock",
72
74
  deliveryResults: [],
73
75
  };
74
- mock.module('../notifications/emit-signal.js', () => ({
76
+ mock.module("../notifications/emit-signal.js", () => ({
75
77
  emitNotificationSignal: async (params: Record<string, unknown>) => {
76
78
  emitSignalCalls.push(params);
77
79
  return mockEmitResult;
@@ -79,9 +81,15 @@ mock.module('../notifications/emit-signal.js', () => ({
79
81
  }));
80
82
 
81
83
  // Track deliverChannelReply calls
82
- const deliverReplyCalls: Array<{ url: string; payload: Record<string, unknown> }> = [];
83
- mock.module('../runtime/gateway-client.js', () => ({
84
- deliverChannelReply: async (url: string, payload: Record<string, unknown>) => {
84
+ const deliverReplyCalls: Array<{
85
+ url: string;
86
+ payload: Record<string, unknown>;
87
+ }> = [];
88
+ mock.module("../runtime/gateway-client.js", () => ({
89
+ deliverChannelReply: async (
90
+ url: string,
91
+ payload: Record<string, unknown>,
92
+ ) => {
85
93
  deliverReplyCalls.push({ url, payload });
86
94
  },
87
95
  }));
@@ -89,43 +97,45 @@ mock.module('../runtime/gateway-client.js', () => ({
89
97
  import {
90
98
  listCanonicalGuardianDeliveries,
91
99
  listCanonicalGuardianRequests,
92
- } from '../memory/canonical-guardian-store.js';
93
- import {
94
- createBinding,
95
- } from '../memory/channel-guardian-store.js';
96
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
97
- import { notifyGuardianOfAccessRequest } from '../runtime/access-request-helper.js';
98
- import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
100
+ } from "../memory/canonical-guardian-store.js";
101
+ import { createBinding } from "../memory/channel-guardian-store.js";
102
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
103
+ import { notifyGuardianOfAccessRequest } from "../runtime/access-request-helper.js";
104
+ import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
99
105
 
100
106
  initializeDb();
101
107
 
102
108
  afterAll(() => {
103
109
  resetDb();
104
- try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
110
+ try {
111
+ rmSync(testDir, { recursive: true });
112
+ } catch {
113
+ /* best effort */
114
+ }
105
115
  });
106
116
 
107
117
  // ---------------------------------------------------------------------------
108
118
  // Helpers
109
119
  // ---------------------------------------------------------------------------
110
120
 
111
- const TEST_BEARER_TOKEN = 'test-token';
121
+ const TEST_BEARER_TOKEN = "test-token";
112
122
 
113
123
  function resetState(): void {
114
124
  const db = getDb();
115
- db.run('DELETE FROM channel_guardian_approval_requests');
116
- db.run('DELETE FROM channel_guardian_bindings');
117
- db.run('DELETE FROM channel_inbound_events');
118
- db.run('DELETE FROM conversations');
119
- db.run('DELETE FROM notification_events');
120
- db.run('DELETE FROM canonical_guardian_requests');
121
- db.run('DELETE FROM canonical_guardian_deliveries');
125
+ db.run("DELETE FROM channel_guardian_approval_requests");
126
+ db.run("DELETE FROM channel_guardian_bindings");
127
+ db.run("DELETE FROM channel_inbound_events");
128
+ db.run("DELETE FROM conversations");
129
+ db.run("DELETE FROM notification_events");
130
+ db.run("DELETE FROM canonical_guardian_requests");
131
+ db.run("DELETE FROM canonical_guardian_deliveries");
122
132
  emitSignalCalls.length = 0;
123
133
  deliverReplyCalls.length = 0;
124
134
  mockEmitResult = {
125
- signalId: 'mock-signal-id',
135
+ signalId: "mock-signal-id",
126
136
  deduplicated: false,
127
137
  dispatched: true,
128
- reason: 'mock',
138
+ reason: "mock",
129
139
  deliveryResults: [],
130
140
  };
131
141
  }
@@ -136,23 +146,23 @@ async function flushAsyncAccessRequestBookkeeping(): Promise<void> {
136
146
 
137
147
  function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
138
148
  const body: Record<string, unknown> = {
139
- sourceChannel: 'telegram',
140
- interface: 'telegram',
141
- conversationExternalId: 'chat-123',
149
+ sourceChannel: "telegram",
150
+ interface: "telegram",
151
+ conversationExternalId: "chat-123",
142
152
  externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
143
- content: 'Hello, can I use this assistant?',
144
- actorExternalId: 'user-unknown-456',
145
- actorDisplayName: 'Alice Unknown',
146
- actorUsername: 'alice_unknown',
147
- replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
153
+ content: "Hello, can I use this assistant?",
154
+ actorExternalId: "user-unknown-456",
155
+ actorDisplayName: "Alice Unknown",
156
+ actorUsername: "alice_unknown",
157
+ replyCallbackUrl: "http://localhost:7830/deliver/telegram",
148
158
  ...overrides,
149
159
  };
150
160
 
151
- return new Request('http://localhost:8080/channels/inbound', {
152
- method: 'POST',
161
+ return new Request("http://localhost:8080/channels/inbound", {
162
+ method: "POST",
153
163
  headers: {
154
- 'Content-Type': 'application/json',
155
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
164
+ "Content-Type": "application/json",
165
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
156
166
  },
157
167
  body: JSON.stringify(body),
158
168
  });
@@ -162,75 +172,80 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
162
172
  // Tests
163
173
  // ---------------------------------------------------------------------------
164
174
 
165
- describe('non-member access request notification', () => {
175
+ describe("non-member access request notification", () => {
166
176
  beforeEach(() => {
167
177
  resetState();
168
178
  });
169
179
 
170
- test('non-member message is denied with rejection reply', async () => {
180
+ test("non-member message is denied with rejection reply", async () => {
171
181
  const req = buildInboundRequest();
172
182
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
173
- const json = await resp.json() as Record<string, unknown>;
183
+ const json = (await resp.json()) as Record<string, unknown>;
174
184
 
175
185
  expect(json.denied).toBe(true);
176
- expect(json.reason).toBe('not_a_member');
186
+ expect(json.reason).toBe("not_a_member");
177
187
 
178
188
  // Rejection reply was delivered — always-notify behavior means the reply
179
189
  // indicates the guardian will be notified, even without a same-channel binding.
180
190
  expect(deliverReplyCalls.length).toBe(1);
181
- expect((deliverReplyCalls[0].payload as Record<string, unknown>).text).toContain("let them know");
191
+ expect(
192
+ (deliverReplyCalls[0].payload as Record<string, unknown>).text,
193
+ ).toContain("let them know");
182
194
  });
183
195
 
184
- test('guardian is notified when a non-member messages and a guardian binding exists', async () => {
196
+ test("guardian is notified when a non-member messages and a guardian binding exists", async () => {
185
197
  // Set up a guardian binding for this channel
186
198
  createBinding({
187
- assistantId: 'self',
188
- channel: 'telegram',
189
- guardianExternalUserId: 'guardian-user-789',
190
- guardianDeliveryChatId: 'guardian-chat-789',
191
- guardianPrincipalId: 'test-principal-id',
199
+ assistantId: "self",
200
+ channel: "telegram",
201
+ guardianExternalUserId: "guardian-user-789",
202
+ guardianDeliveryChatId: "guardian-chat-789",
203
+ guardianPrincipalId: "test-principal-id",
192
204
  });
193
205
 
194
206
  const req = buildInboundRequest();
195
207
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
196
- const json = await resp.json() as Record<string, unknown>;
208
+ const json = (await resp.json()) as Record<string, unknown>;
197
209
 
198
210
  // Message is still denied
199
211
  expect(json.denied).toBe(true);
200
- expect(json.reason).toBe('not_a_member');
212
+ expect(json.reason).toBe("not_a_member");
201
213
 
202
214
  // Rejection reply was delivered
203
215
  expect(deliverReplyCalls.length).toBe(1);
204
216
 
205
217
  // A notification signal was emitted
206
218
  expect(emitSignalCalls.length).toBe(1);
207
- expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
208
- expect(emitSignalCalls[0].sourceChannel).toBe('telegram');
209
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
210
- expect(payload.actorExternalId).toBe('user-unknown-456');
211
- expect(payload.actorDisplayName).toBe('Alice Unknown');
219
+ expect(emitSignalCalls[0].sourceEventName).toBe("ingress.access_request");
220
+ expect(emitSignalCalls[0].sourceChannel).toBe("telegram");
221
+ const payload = emitSignalCalls[0].contextPayload as Record<
222
+ string,
223
+ unknown
224
+ >;
225
+ expect(payload.actorExternalId).toBe("user-unknown-456");
226
+ expect(payload.actorDisplayName).toBe("Alice Unknown");
212
227
 
213
228
  // A canonical access request was created
214
229
  const pending = listCanonicalGuardianRequests({
215
- status: 'pending',
216
- requesterExternalUserId: 'user-unknown-456',
217
- sourceChannel: 'telegram',
218
- kind: 'access_request',
230
+ status: "pending",
231
+ requesterExternalUserId: "user-unknown-456",
232
+ sourceChannel: "telegram",
233
+ kind: "access_request",
219
234
  });
220
235
  expect(pending.length).toBe(1);
221
- expect(pending[0].status).toBe('pending');
222
- expect(pending[0].requesterExternalUserId).toBe('user-unknown-456');
223
- expect(pending[0].guardianExternalUserId).toBe('guardian-user-789');
224
- expect(pending[0].toolName).toBe('ingress_access_request');
236
+ expect(pending[0].status).toBe("pending");
237
+ expect(pending[0].requesterExternalUserId).toBe("user-unknown-456");
238
+ expect(pending[0].guardianExternalUserId).toBe("guardian-user-789");
239
+ expect(pending[0].toolName).toBe("ingress_access_request");
225
240
  });
226
241
 
227
- test('no duplicate approval requests for repeated messages from same non-member', async () => {
242
+ test("no duplicate approval requests for repeated messages from same non-member", async () => {
228
243
  createBinding({
229
- assistantId: 'self',
230
- channel: 'telegram',
231
- guardianExternalUserId: 'guardian-user-789',
232
- guardianDeliveryChatId: 'guardian-chat-789',
233
- guardianPrincipalId: 'test-principal-id',
244
+ assistantId: "self",
245
+ channel: "telegram",
246
+ guardianExternalUserId: "guardian-user-789",
247
+ guardianDeliveryChatId: "guardian-chat-789",
248
+ guardianPrincipalId: "test-principal-id",
234
249
  });
235
250
 
236
251
  // First message
@@ -240,7 +255,7 @@ describe('non-member access request notification', () => {
240
255
  // Second message from the same user
241
256
  const req2 = buildInboundRequest({
242
257
  externalMessageId: `msg-second-${Date.now()}`,
243
- content: 'Please let me in!',
258
+ content: "Please let me in!",
244
259
  });
245
260
  await handleChannelInbound(req2, undefined, TEST_BEARER_TOKEN);
246
261
 
@@ -252,38 +267,40 @@ describe('non-member access request notification', () => {
252
267
 
253
268
  // Only one canonical request should exist
254
269
  const pending = listCanonicalGuardianRequests({
255
- status: 'pending',
256
- requesterExternalUserId: 'user-unknown-456',
257
- sourceChannel: 'telegram',
258
- kind: 'access_request',
270
+ status: "pending",
271
+ requesterExternalUserId: "user-unknown-456",
272
+ sourceChannel: "telegram",
273
+ kind: "access_request",
259
274
  });
260
275
  expect(pending.length).toBe(1);
261
276
  });
262
277
 
263
- test('access request is created with self-healed principal even without same-channel guardian binding', async () => {
278
+ test("access request is created with self-healed principal even without same-channel guardian binding", async () => {
264
279
  // No guardian binding on any channel — self-heal creates a vellum binding
265
280
  // so the access_request (now decisionable) has a guardianPrincipalId.
266
281
  const req = buildInboundRequest();
267
282
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
268
- const json = await resp.json() as Record<string, unknown>;
283
+ const json = (await resp.json()) as Record<string, unknown>;
269
284
 
270
285
  expect(json.denied).toBe(true);
271
- expect(json.reason).toBe('not_a_member');
286
+ expect(json.reason).toBe("not_a_member");
272
287
 
273
288
  // Rejection reply indicates guardian was notified
274
289
  expect(deliverReplyCalls.length).toBe(1);
275
- expect((deliverReplyCalls[0].payload as Record<string, unknown>).text).toContain("let them know");
290
+ expect(
291
+ (deliverReplyCalls[0].payload as Record<string, unknown>).text,
292
+ ).toContain("let them know");
276
293
 
277
294
  // Notification signal was emitted
278
295
  expect(emitSignalCalls.length).toBe(1);
279
- expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
296
+ expect(emitSignalCalls[0].sourceEventName).toBe("ingress.access_request");
280
297
 
281
298
  // Canonical request was created with a self-healed principal
282
299
  const pending = listCanonicalGuardianRequests({
283
- status: 'pending',
284
- requesterExternalUserId: 'user-unknown-456',
285
- sourceChannel: 'telegram',
286
- kind: 'access_request',
300
+ status: "pending",
301
+ requesterExternalUserId: "user-unknown-456",
302
+ sourceChannel: "telegram",
303
+ kind: "access_request",
287
304
  });
288
305
  expect(pending.length).toBe(1);
289
306
  // Self-heal bootstraps a vellum binding — guardianExternalUserId is now set
@@ -291,46 +308,49 @@ describe('non-member access request notification', () => {
291
308
  expect(pending[0].guardianPrincipalId).toBeDefined();
292
309
  });
293
310
 
294
- test('cross-channel fallback: SMS guardian binding resolves for Telegram access request', async () => {
311
+ test("cross-channel fallback: SMS guardian binding resolves for Telegram access request", async () => {
295
312
  // Only an SMS guardian binding exists — no Telegram binding
296
313
  createBinding({
297
- assistantId: 'self',
298
- channel: 'sms',
299
- guardianExternalUserId: 'guardian-sms-user',
300
- guardianDeliveryChatId: 'guardian-sms-chat',
301
- guardianPrincipalId: 'test-principal-id',
314
+ assistantId: "self",
315
+ channel: "sms",
316
+ guardianExternalUserId: "guardian-sms-user",
317
+ guardianDeliveryChatId: "guardian-sms-chat",
318
+ guardianPrincipalId: "test-principal-id",
302
319
  });
303
320
 
304
321
  const req = buildInboundRequest();
305
322
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
306
- const json = await resp.json() as Record<string, unknown>;
323
+ const json = (await resp.json()) as Record<string, unknown>;
307
324
 
308
325
  expect(json.denied).toBe(true);
309
- expect(json.reason).toBe('not_a_member');
326
+ expect(json.reason).toBe("not_a_member");
310
327
 
311
328
  // Notification signal emitted
312
329
  expect(emitSignalCalls.length).toBe(1);
313
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
314
- expect(payload.guardianBindingChannel).toBe('sms');
330
+ const payload = emitSignalCalls[0].contextPayload as Record<
331
+ string,
332
+ unknown
333
+ >;
334
+ expect(payload.guardianBindingChannel).toBe("sms");
315
335
 
316
336
  // Canonical request has the SMS guardian's external user ID
317
337
  const pending = listCanonicalGuardianRequests({
318
- status: 'pending',
319
- requesterExternalUserId: 'user-unknown-456',
320
- sourceChannel: 'telegram',
321
- kind: 'access_request',
338
+ status: "pending",
339
+ requesterExternalUserId: "user-unknown-456",
340
+ sourceChannel: "telegram",
341
+ kind: "access_request",
322
342
  });
323
343
  expect(pending.length).toBe(1);
324
- expect(pending[0].guardianExternalUserId).toBe('guardian-sms-user');
344
+ expect(pending[0].guardianExternalUserId).toBe("guardian-sms-user");
325
345
  });
326
346
 
327
- test('no notification when actorExternalId is absent', async () => {
347
+ test("no notification when actorExternalId is absent", async () => {
328
348
  createBinding({
329
- assistantId: 'self',
330
- channel: 'telegram',
331
- guardianExternalUserId: 'guardian-user-789',
332
- guardianDeliveryChatId: 'guardian-chat-789',
333
- guardianPrincipalId: 'test-principal-id',
349
+ assistantId: "self",
350
+ channel: "telegram",
351
+ guardianExternalUserId: "guardian-user-789",
352
+ guardianDeliveryChatId: "guardian-chat-789",
353
+ guardianPrincipalId: "test-principal-id",
334
354
  });
335
355
 
336
356
  // Message without actorExternalId — the handler returns BAD_REQUEST.
@@ -345,36 +365,39 @@ describe('non-member access request notification', () => {
345
365
  });
346
366
  });
347
367
 
348
- describe('access-request-helper unit tests', () => {
368
+ describe("access-request-helper unit tests", () => {
349
369
  beforeEach(() => {
350
370
  resetState();
351
371
  });
352
372
 
353
- test('notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent', () => {
373
+ test("notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent", () => {
354
374
  const result = notifyGuardianOfAccessRequest({
355
- canonicalAssistantId: 'self',
356
- sourceChannel: 'telegram',
357
- conversationExternalId: 'chat-123',
375
+ canonicalAssistantId: "self",
376
+ sourceChannel: "telegram",
377
+ conversationExternalId: "chat-123",
358
378
  actorExternalId: undefined,
359
379
  });
360
380
 
361
381
  expect(result.notified).toBe(false);
362
382
  if (!result.notified) {
363
- expect(result.reason).toBe('no_sender_id');
383
+ expect(result.reason).toBe("no_sender_id");
364
384
  }
365
385
 
366
386
  // No canonical request created
367
- const pending = listCanonicalGuardianRequests({ status: 'pending', kind: 'access_request' });
387
+ const pending = listCanonicalGuardianRequests({
388
+ status: "pending",
389
+ kind: "access_request",
390
+ });
368
391
  expect(pending.length).toBe(0);
369
392
  });
370
393
 
371
- test('notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists', () => {
394
+ test("notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists", () => {
372
395
  const result = notifyGuardianOfAccessRequest({
373
- canonicalAssistantId: 'self',
374
- sourceChannel: 'telegram',
375
- conversationExternalId: 'chat-123',
376
- actorExternalId: 'unknown-user',
377
- actorDisplayName: 'Bob',
396
+ canonicalAssistantId: "self",
397
+ sourceChannel: "telegram",
398
+ conversationExternalId: "chat-123",
399
+ actorExternalId: "unknown-user",
400
+ actorDisplayName: "Bob",
378
401
  });
379
402
 
380
403
  expect(result.notified).toBe(true);
@@ -383,9 +406,9 @@ describe('access-request-helper unit tests', () => {
383
406
  }
384
407
 
385
408
  const pending = listCanonicalGuardianRequests({
386
- status: 'pending',
387
- requesterExternalUserId: 'unknown-user',
388
- kind: 'access_request',
409
+ status: "pending",
410
+ requesterExternalUserId: "unknown-user",
411
+ kind: "access_request",
389
412
  });
390
413
  expect(pending.length).toBe(1);
391
414
  // Self-heal bootstraps a vellum binding
@@ -396,167 +419,182 @@ describe('access-request-helper unit tests', () => {
396
419
  expect(emitSignalCalls.length).toBe(1);
397
420
  });
398
421
 
399
- test('notifyGuardianOfAccessRequest uses cross-channel binding when source-channel binding is missing', () => {
422
+ test("notifyGuardianOfAccessRequest uses cross-channel binding when source-channel binding is missing", () => {
400
423
  // Only SMS binding exists
401
424
  createBinding({
402
- assistantId: 'self',
403
- channel: 'sms',
404
- guardianExternalUserId: 'guardian-sms',
405
- guardianDeliveryChatId: 'sms-chat',
406
- guardianPrincipalId: 'test-principal-id',
425
+ assistantId: "self",
426
+ channel: "sms",
427
+ guardianExternalUserId: "guardian-sms",
428
+ guardianDeliveryChatId: "sms-chat",
429
+ guardianPrincipalId: "test-principal-id",
407
430
  });
408
431
 
409
432
  const result = notifyGuardianOfAccessRequest({
410
- canonicalAssistantId: 'self',
411
- sourceChannel: 'telegram',
412
- conversationExternalId: 'tg-chat',
413
- actorExternalId: 'unknown-tg-user',
433
+ canonicalAssistantId: "self",
434
+ sourceChannel: "telegram",
435
+ conversationExternalId: "tg-chat",
436
+ actorExternalId: "unknown-tg-user",
414
437
  });
415
438
 
416
439
  expect(result.notified).toBe(true);
417
440
 
418
441
  const pending = listCanonicalGuardianRequests({
419
- status: 'pending',
420
- requesterExternalUserId: 'unknown-tg-user',
421
- kind: 'access_request',
442
+ status: "pending",
443
+ requesterExternalUserId: "unknown-tg-user",
444
+ kind: "access_request",
422
445
  });
423
446
  expect(pending.length).toBe(1);
424
- expect(pending[0].guardianExternalUserId).toBe('guardian-sms');
447
+ expect(pending[0].guardianExternalUserId).toBe("guardian-sms");
425
448
 
426
449
  // Signal payload includes fallback channel
427
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
428
- expect(payload.guardianBindingChannel).toBe('sms');
450
+ const payload = emitSignalCalls[0].contextPayload as Record<
451
+ string,
452
+ unknown
453
+ >;
454
+ expect(payload.guardianBindingChannel).toBe("sms");
429
455
  });
430
456
 
431
- test('notifyGuardianOfAccessRequest prefers source-channel binding over cross-channel fallback', () => {
457
+ test("notifyGuardianOfAccessRequest prefers source-channel binding over cross-channel fallback", () => {
432
458
  // Both Telegram and SMS bindings exist
433
459
  createBinding({
434
- assistantId: 'self',
435
- channel: 'telegram',
436
- guardianExternalUserId: 'guardian-tg',
437
- guardianDeliveryChatId: 'tg-chat',
438
- guardianPrincipalId: 'test-principal-tg',
460
+ assistantId: "self",
461
+ channel: "telegram",
462
+ guardianExternalUserId: "guardian-tg",
463
+ guardianDeliveryChatId: "tg-chat",
464
+ guardianPrincipalId: "test-principal-tg",
439
465
  });
440
466
  createBinding({
441
- assistantId: 'self',
442
- channel: 'sms',
443
- guardianExternalUserId: 'guardian-sms',
444
- guardianDeliveryChatId: 'sms-chat',
445
- guardianPrincipalId: 'test-principal-sms',
467
+ assistantId: "self",
468
+ channel: "sms",
469
+ guardianExternalUserId: "guardian-sms",
470
+ guardianDeliveryChatId: "sms-chat",
471
+ guardianPrincipalId: "test-principal-sms",
446
472
  });
447
473
 
448
474
  const result = notifyGuardianOfAccessRequest({
449
- canonicalAssistantId: 'self',
450
- sourceChannel: 'telegram',
451
- conversationExternalId: 'chat-123',
452
- actorExternalId: 'unknown-user',
475
+ canonicalAssistantId: "self",
476
+ sourceChannel: "telegram",
477
+ conversationExternalId: "chat-123",
478
+ actorExternalId: "unknown-user",
453
479
  });
454
480
 
455
481
  expect(result.notified).toBe(true);
456
482
 
457
483
  const pending = listCanonicalGuardianRequests({
458
- status: 'pending',
459
- requesterExternalUserId: 'unknown-user',
460
- kind: 'access_request',
484
+ status: "pending",
485
+ requesterExternalUserId: "unknown-user",
486
+ kind: "access_request",
461
487
  });
462
488
  expect(pending.length).toBe(1);
463
489
  // Should use the Telegram binding, not SMS fallback
464
- expect(pending[0].guardianExternalUserId).toBe('guardian-tg');
490
+ expect(pending[0].guardianExternalUserId).toBe("guardian-tg");
465
491
 
466
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
467
- expect(payload.guardianBindingChannel).toBe('telegram');
492
+ const payload = emitSignalCalls[0].contextPayload as Record<
493
+ string,
494
+ unknown
495
+ >;
496
+ expect(payload.guardianBindingChannel).toBe("telegram");
468
497
  });
469
498
 
470
- test('notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload', () => {
499
+ test("notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload", () => {
471
500
  const result = notifyGuardianOfAccessRequest({
472
- canonicalAssistantId: 'self',
473
- sourceChannel: 'voice',
474
- conversationExternalId: '+15559998888',
475
- actorExternalId: '+15559998888',
476
- actorDisplayName: 'Alice Caller',
501
+ canonicalAssistantId: "self",
502
+ sourceChannel: "voice",
503
+ conversationExternalId: "+15559998888",
504
+ actorExternalId: "+15559998888",
505
+ actorDisplayName: "Alice Caller",
477
506
  });
478
507
 
479
508
  expect(result.notified).toBe(true);
480
509
  expect(emitSignalCalls.length).toBe(1);
481
510
 
482
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
483
- expect(payload.sourceChannel).toBe('voice');
484
- expect(payload.actorDisplayName).toBe('Alice Caller');
485
- expect(payload.actorExternalId).toBe('+15559998888');
486
- expect(payload.senderIdentifier).toBe('Alice Caller');
511
+ const payload = emitSignalCalls[0].contextPayload as Record<
512
+ string,
513
+ unknown
514
+ >;
515
+ expect(payload.sourceChannel).toBe("voice");
516
+ expect(payload.actorDisplayName).toBe("Alice Caller");
517
+ expect(payload.actorExternalId).toBe("+15559998888");
518
+ expect(payload.senderIdentifier).toBe("Alice Caller");
487
519
 
488
520
  // Canonical request should exist
489
521
  const pending = listCanonicalGuardianRequests({
490
- status: 'pending',
491
- requesterExternalUserId: '+15559998888',
492
- sourceChannel: 'voice',
493
- kind: 'access_request',
522
+ status: "pending",
523
+ requesterExternalUserId: "+15559998888",
524
+ sourceChannel: "voice",
525
+ kind: "access_request",
494
526
  });
495
527
  expect(pending.length).toBe(1);
496
528
  });
497
529
 
498
- test('notifyGuardianOfAccessRequest includes requestCode in contextPayload', () => {
530
+ test("notifyGuardianOfAccessRequest includes requestCode in contextPayload", () => {
499
531
  const result = notifyGuardianOfAccessRequest({
500
- canonicalAssistantId: 'self',
501
- sourceChannel: 'telegram',
502
- conversationExternalId: 'chat-123',
503
- actorExternalId: 'unknown-user',
504
- actorDisplayName: 'Test User',
532
+ canonicalAssistantId: "self",
533
+ sourceChannel: "telegram",
534
+ conversationExternalId: "chat-123",
535
+ actorExternalId: "unknown-user",
536
+ actorDisplayName: "Test User",
505
537
  });
506
538
 
507
539
  expect(result.notified).toBe(true);
508
540
  expect(emitSignalCalls.length).toBe(1);
509
541
 
510
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
542
+ const payload = emitSignalCalls[0].contextPayload as Record<
543
+ string,
544
+ unknown
545
+ >;
511
546
  expect(payload.requestCode).toBeDefined();
512
- expect(typeof payload.requestCode).toBe('string');
547
+ expect(typeof payload.requestCode).toBe("string");
513
548
  expect((payload.requestCode as string).length).toBe(6);
514
549
  });
515
550
 
516
- test('notifyGuardianOfAccessRequest includes previousMemberStatus in contextPayload', () => {
551
+ test("notifyGuardianOfAccessRequest includes previousMemberStatus in contextPayload", () => {
517
552
  const result = notifyGuardianOfAccessRequest({
518
- canonicalAssistantId: 'self',
519
- sourceChannel: 'telegram',
520
- conversationExternalId: 'chat-123',
521
- actorExternalId: 'revoked-user',
522
- actorDisplayName: 'Revoked User',
523
- previousMemberStatus: 'revoked',
553
+ canonicalAssistantId: "self",
554
+ sourceChannel: "telegram",
555
+ conversationExternalId: "chat-123",
556
+ actorExternalId: "revoked-user",
557
+ actorDisplayName: "Revoked User",
558
+ previousMemberStatus: "revoked",
524
559
  });
525
560
 
526
561
  expect(result.notified).toBe(true);
527
562
  expect(emitSignalCalls.length).toBe(1);
528
563
 
529
- const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
530
- expect(payload.previousMemberStatus).toBe('revoked');
564
+ const payload = emitSignalCalls[0].contextPayload as Record<
565
+ string,
566
+ unknown
567
+ >;
568
+ expect(payload.previousMemberStatus).toBe("revoked");
531
569
  });
532
570
 
533
- test('notifyGuardianOfAccessRequest persists canonical delivery rows from notification results', async () => {
571
+ test("notifyGuardianOfAccessRequest persists canonical delivery rows from notification results", async () => {
534
572
  mockEmitResult = {
535
- signalId: 'sig-deliveries',
573
+ signalId: "sig-deliveries",
536
574
  deduplicated: false,
537
575
  dispatched: true,
538
- reason: 'ok',
576
+ reason: "ok",
539
577
  deliveryResults: [
540
578
  {
541
- channel: 'vellum',
542
- destination: 'vellum',
543
- status: 'sent',
544
- conversationId: 'conv-guardian-access-request',
579
+ channel: "vellum",
580
+ destination: "vellum",
581
+ status: "sent",
582
+ conversationId: "conv-guardian-access-request",
545
583
  },
546
584
  {
547
- channel: 'telegram',
548
- destination: 'guardian-chat-123',
549
- status: 'sent',
585
+ channel: "telegram",
586
+ destination: "guardian-chat-123",
587
+ status: "sent",
550
588
  },
551
589
  ],
552
590
  };
553
591
 
554
592
  const result = notifyGuardianOfAccessRequest({
555
- canonicalAssistantId: 'self',
556
- sourceChannel: 'voice',
557
- conversationExternalId: '+15556667777',
558
- actorExternalId: '+15556667777',
559
- actorDisplayName: 'Noah',
593
+ canonicalAssistantId: "self",
594
+ sourceChannel: "voice",
595
+ conversationExternalId: "+15556667777",
596
+ actorExternalId: "+15556667777",
597
+ actorDisplayName: "Noah",
560
598
  });
561
599
 
562
600
  expect(result.notified).toBe(true);
@@ -565,38 +603,42 @@ describe('access-request-helper unit tests', () => {
565
603
  await flushAsyncAccessRequestBookkeeping();
566
604
 
567
605
  const deliveries = listCanonicalGuardianDeliveries(result.requestId);
568
- const vellum = deliveries.find((d) => d.destinationChannel === 'vellum');
569
- const telegram = deliveries.find((d) => d.destinationChannel === 'telegram');
606
+ const vellum = deliveries.find((d) => d.destinationChannel === "vellum");
607
+ const telegram = deliveries.find(
608
+ (d) => d.destinationChannel === "telegram",
609
+ );
570
610
 
571
611
  expect(vellum).toBeDefined();
572
- expect(vellum!.destinationConversationId).toBe('conv-guardian-access-request');
573
- expect(vellum!.status).toBe('sent');
612
+ expect(vellum!.destinationConversationId).toBe(
613
+ "conv-guardian-access-request",
614
+ );
615
+ expect(vellum!.status).toBe("sent");
574
616
  expect(telegram).toBeDefined();
575
- expect(telegram!.destinationChatId).toBe('guardian-chat-123');
576
- expect(telegram!.status).toBe('sent');
617
+ expect(telegram!.destinationChatId).toBe("guardian-chat-123");
618
+ expect(telegram!.status).toBe("sent");
577
619
  });
578
620
 
579
- test('notifyGuardianOfAccessRequest records failed vellum fallback when pipeline has no vellum delivery', async () => {
621
+ test("notifyGuardianOfAccessRequest records failed vellum fallback when pipeline has no vellum delivery", async () => {
580
622
  mockEmitResult = {
581
- signalId: 'sig-no-vellum',
623
+ signalId: "sig-no-vellum",
582
624
  deduplicated: false,
583
625
  dispatched: true,
584
- reason: 'telegram-only',
626
+ reason: "telegram-only",
585
627
  deliveryResults: [
586
628
  {
587
- channel: 'telegram',
588
- destination: 'guardian-chat-456',
589
- status: 'sent',
629
+ channel: "telegram",
630
+ destination: "guardian-chat-456",
631
+ status: "sent",
590
632
  },
591
633
  ],
592
634
  };
593
635
 
594
636
  const result = notifyGuardianOfAccessRequest({
595
- canonicalAssistantId: 'self',
596
- sourceChannel: 'telegram',
597
- conversationExternalId: 'chat-123',
598
- actorExternalId: 'unknown-user',
599
- actorDisplayName: 'Alice',
637
+ canonicalAssistantId: "self",
638
+ sourceChannel: "telegram",
639
+ conversationExternalId: "chat-123",
640
+ actorExternalId: "unknown-user",
641
+ actorDisplayName: "Alice",
600
642
  });
601
643
 
602
644
  expect(result.notified).toBe(true);
@@ -605,13 +647,15 @@ describe('access-request-helper unit tests', () => {
605
647
  await flushAsyncAccessRequestBookkeeping();
606
648
 
607
649
  const deliveries = listCanonicalGuardianDeliveries(result.requestId);
608
- const vellum = deliveries.find((d) => d.destinationChannel === 'vellum');
609
- const telegram = deliveries.find((d) => d.destinationChannel === 'telegram');
650
+ const vellum = deliveries.find((d) => d.destinationChannel === "vellum");
651
+ const telegram = deliveries.find(
652
+ (d) => d.destinationChannel === "telegram",
653
+ );
610
654
 
611
655
  expect(vellum).toBeDefined();
612
- expect(vellum!.status).toBe('failed');
656
+ expect(vellum!.status).toBe("failed");
613
657
  expect(telegram).toBeDefined();
614
- expect(telegram!.destinationChatId).toBe('guardian-chat-456');
615
- expect(telegram!.status).toBe('sent');
658
+ expect(telegram!.destinationChatId).toBe("guardian-chat-456");
659
+ expect(telegram!.status).toBe("sent");
616
660
  });
617
661
  });