@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
5
 
6
- const testDir = mkdtempSync(join(tmpdir(), "ingress-routes-http-test-"));
6
+ const testDir = mkdtempSync(join(tmpdir(), "invite-routes-http-test-"));
7
7
 
8
8
  mock.module("../util/platform.js", () => ({
9
9
  getDataDir: () => testDir,
@@ -26,15 +26,11 @@ mock.module("../util/logger.js", () => ({
26
26
 
27
27
  import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
28
28
  import {
29
- handleBlockMember,
30
29
  handleCreateInvite,
31
30
  handleListInvites,
32
- handleListMembers,
33
31
  handleRedeemInvite,
34
32
  handleRevokeInvite,
35
- handleRevokeMember,
36
- handleUpsertMember,
37
- } from "../runtime/routes/ingress-routes.js";
33
+ } from "../runtime/routes/invite-routes.js";
38
34
 
39
35
  initializeDb();
40
36
 
@@ -53,253 +49,6 @@ function resetTables() {
53
49
  getSqlite().run("DELETE FROM contacts");
54
50
  }
55
51
 
56
- // ---------------------------------------------------------------------------
57
- // Member routes
58
- // ---------------------------------------------------------------------------
59
-
60
- describe("ingress member HTTP routes", () => {
61
- beforeEach(resetTables);
62
-
63
- test("POST /v1/ingress/members — upsert creates a member", async () => {
64
- const req = new Request("http://localhost/v1/ingress/members", {
65
- method: "POST",
66
- headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({
68
- sourceChannel: "telegram",
69
- externalUserId: "user-1",
70
- displayName: "Test User",
71
- policy: "allow",
72
- status: "active",
73
- }),
74
- });
75
-
76
- const res = await handleUpsertMember(req);
77
- const body = (await res.json()) as Record<string, unknown>;
78
-
79
- expect(res.status).toBe(200);
80
- expect(body.ok).toBe(true);
81
- expect(body.member).toBeDefined();
82
- const member = body.member as Record<string, unknown>;
83
- expect(member.sourceChannel).toBe("telegram");
84
- expect(member.externalUserId).toBe("user-1");
85
- expect(member.displayName).toBe("Test User");
86
- expect(member.policy).toBe("allow");
87
- expect(member.status).toBe("active");
88
- });
89
-
90
- test("POST /v1/ingress/members — missing sourceChannel returns 400", async () => {
91
- const req = new Request("http://localhost/v1/ingress/members", {
92
- method: "POST",
93
- headers: { "Content-Type": "application/json" },
94
- body: JSON.stringify({
95
- externalUserId: "user-1",
96
- }),
97
- });
98
-
99
- const res = await handleUpsertMember(req);
100
- const body = (await res.json()) as { ok: boolean; error: string };
101
-
102
- expect(res.status).toBe(400);
103
- expect(body.ok).toBe(false);
104
- expect(body.error).toContain("sourceChannel");
105
- });
106
-
107
- test("POST /v1/ingress/members — missing identity returns 400", async () => {
108
- const req = new Request("http://localhost/v1/ingress/members", {
109
- method: "POST",
110
- headers: { "Content-Type": "application/json" },
111
- body: JSON.stringify({
112
- sourceChannel: "telegram",
113
- }),
114
- });
115
-
116
- const res = await handleUpsertMember(req);
117
- const body = (await res.json()) as { ok: boolean; error: string };
118
-
119
- expect(res.status).toBe(400);
120
- expect(body.ok).toBe(false);
121
- expect(body.error).toContain("externalUserId");
122
- });
123
-
124
- test("GET /v1/ingress/members — lists members", async () => {
125
- // Create two members
126
- await handleUpsertMember(
127
- new Request("http://localhost/v1/ingress/members", {
128
- method: "POST",
129
- headers: { "Content-Type": "application/json" },
130
- body: JSON.stringify({
131
- sourceChannel: "telegram",
132
- externalUserId: "user-1",
133
- status: "active",
134
- }),
135
- }),
136
- );
137
- await handleUpsertMember(
138
- new Request("http://localhost/v1/ingress/members", {
139
- method: "POST",
140
- headers: { "Content-Type": "application/json" },
141
- body: JSON.stringify({
142
- sourceChannel: "telegram",
143
- externalUserId: "user-2",
144
- status: "active",
145
- }),
146
- }),
147
- );
148
-
149
- const url = new URL("http://localhost/v1/ingress/members");
150
- const res = handleListMembers(url);
151
- const body = (await res.json()) as Record<string, unknown>;
152
-
153
- expect(res.status).toBe(200);
154
- expect(body.ok).toBe(true);
155
- expect(Array.isArray(body.members)).toBe(true);
156
- expect((body.members as unknown[]).length).toBe(2);
157
- });
158
-
159
- test("GET /v1/ingress/members — filters by sourceChannel", async () => {
160
- await handleUpsertMember(
161
- new Request("http://localhost/v1/ingress/members", {
162
- method: "POST",
163
- headers: { "Content-Type": "application/json" },
164
- body: JSON.stringify({
165
- sourceChannel: "telegram",
166
- externalUserId: "user-1",
167
- status: "active",
168
- }),
169
- }),
170
- );
171
- await handleUpsertMember(
172
- new Request("http://localhost/v1/ingress/members", {
173
- method: "POST",
174
- headers: { "Content-Type": "application/json" },
175
- body: JSON.stringify({
176
- sourceChannel: "sms",
177
- externalUserId: "user-2",
178
- status: "active",
179
- }),
180
- }),
181
- );
182
-
183
- const url = new URL(
184
- "http://localhost/v1/ingress/members?sourceChannel=telegram",
185
- );
186
- const res = handleListMembers(url);
187
- const body = (await res.json()) as Record<string, unknown>;
188
-
189
- expect((body.members as unknown[]).length).toBe(1);
190
- });
191
-
192
- test("DELETE /v1/ingress/members/:id — revokes a member", async () => {
193
- const createRes = await handleUpsertMember(
194
- new Request("http://localhost/v1/ingress/members", {
195
- method: "POST",
196
- headers: { "Content-Type": "application/json" },
197
- body: JSON.stringify({
198
- sourceChannel: "telegram",
199
- externalUserId: "user-1",
200
- status: "active",
201
- }),
202
- }),
203
- );
204
- const created = (await createRes.json()) as { member: { id: string } };
205
-
206
- const req = new Request(
207
- "http://localhost/v1/ingress/members/" + created.member.id,
208
- {
209
- method: "DELETE",
210
- headers: { "Content-Type": "application/json" },
211
- body: JSON.stringify({ reason: "test revoke" }),
212
- },
213
- );
214
- const res = await handleRevokeMember(req, created.member.id);
215
- const body = (await res.json()) as Record<string, unknown>;
216
-
217
- expect(res.status).toBe(200);
218
- expect(body.ok).toBe(true);
219
- const member = body.member as Record<string, unknown>;
220
- expect(member.status).toBe("revoked");
221
- });
222
-
223
- test("DELETE /v1/ingress/members/:id — not found returns 404", async () => {
224
- const req = new Request("http://localhost/v1/ingress/members/nonexistent", {
225
- method: "DELETE",
226
- });
227
- const res = await handleRevokeMember(req, "nonexistent");
228
- const body = (await res.json()) as Record<string, unknown>;
229
-
230
- expect(res.status).toBe(404);
231
- expect(body.ok).toBe(false);
232
- });
233
-
234
- test("POST /v1/ingress/members/:id/block — blocks a member", async () => {
235
- const createRes = await handleUpsertMember(
236
- new Request("http://localhost/v1/ingress/members", {
237
- method: "POST",
238
- headers: { "Content-Type": "application/json" },
239
- body: JSON.stringify({
240
- sourceChannel: "telegram",
241
- externalUserId: "user-1",
242
- status: "active",
243
- }),
244
- }),
245
- );
246
- const created = (await createRes.json()) as { member: { id: string } };
247
-
248
- const req = new Request(
249
- "http://localhost/v1/ingress/members/" + created.member.id + "/block",
250
- {
251
- method: "POST",
252
- headers: { "Content-Type": "application/json" },
253
- body: JSON.stringify({ reason: "spam" }),
254
- },
255
- );
256
- const res = await handleBlockMember(req, created.member.id);
257
- const body = (await res.json()) as Record<string, unknown>;
258
-
259
- expect(res.status).toBe(200);
260
- expect(body.ok).toBe(true);
261
- const member = body.member as Record<string, unknown>;
262
- expect(member.status).toBe("blocked");
263
- });
264
-
265
- test("POST /v1/ingress/members/:id/block — already blocked returns 404", async () => {
266
- const createRes = await handleUpsertMember(
267
- new Request("http://localhost/v1/ingress/members", {
268
- method: "POST",
269
- headers: { "Content-Type": "application/json" },
270
- body: JSON.stringify({
271
- sourceChannel: "telegram",
272
- externalUserId: "user-1",
273
- status: "active",
274
- }),
275
- }),
276
- );
277
- const created = (await createRes.json()) as { member: { id: string } };
278
-
279
- // Block first time
280
- await handleBlockMember(
281
- new Request("http://localhost/block", {
282
- method: "POST",
283
- headers: { "Content-Type": "application/json" },
284
- body: JSON.stringify({}),
285
- }),
286
- created.member.id,
287
- );
288
-
289
- // Block second time
290
- const req = new Request("http://localhost/block", {
291
- method: "POST",
292
- headers: { "Content-Type": "application/json" },
293
- body: JSON.stringify({}),
294
- });
295
- const res = await handleBlockMember(req, created.member.id);
296
- const body = (await res.json()) as Record<string, unknown>;
297
-
298
- expect(res.status).toBe(404);
299
- expect(body.ok).toBe(false);
300
- });
301
- });
302
-
303
52
  // ---------------------------------------------------------------------------
304
53
  // Invite routes
305
54
  // ---------------------------------------------------------------------------
@@ -307,8 +56,8 @@ describe("ingress member HTTP routes", () => {
307
56
  describe("ingress invite HTTP routes", () => {
308
57
  beforeEach(resetTables);
309
58
 
310
- test("POST /v1/ingress/invites — creates an invite", async () => {
311
- const req = new Request("http://localhost/v1/ingress/invites", {
59
+ test("POST /v1/contacts/invites — creates an invite", async () => {
60
+ const req = new Request("http://localhost/v1/contacts/invites", {
312
61
  method: "POST",
313
62
  headers: { "Content-Type": "application/json" },
314
63
  body: JSON.stringify({
@@ -333,12 +82,12 @@ describe("ingress invite HTTP routes", () => {
333
82
  expect((invite.token as string).length).toBeGreaterThan(0);
334
83
  });
335
84
 
336
- test("POST /v1/ingress/invites — includes canonical share URL when bot username is configured", async () => {
85
+ test("POST /v1/contacts/invites — includes canonical share URL when bot username is configured", async () => {
337
86
  const prevBotUsername = process.env.TELEGRAM_BOT_USERNAME;
338
87
  process.env.TELEGRAM_BOT_USERNAME = "test_invite_bot";
339
88
 
340
89
  try {
341
- const req = new Request("http://localhost/v1/ingress/invites", {
90
+ const req = new Request("http://localhost/v1/contacts/invites", {
342
91
  method: "POST",
343
92
  headers: { "Content-Type": "application/json" },
344
93
  body: JSON.stringify({
@@ -369,8 +118,8 @@ describe("ingress invite HTTP routes", () => {
369
118
  }
370
119
  });
371
120
 
372
- test("POST /v1/ingress/invites — missing sourceChannel returns 400", async () => {
373
- const req = new Request("http://localhost/v1/ingress/invites", {
121
+ test("POST /v1/contacts/invites — missing sourceChannel returns 400", async () => {
122
+ const req = new Request("http://localhost/v1/contacts/invites", {
374
123
  method: "POST",
375
124
  headers: { "Content-Type": "application/json" },
376
125
  body: JSON.stringify({ note: "No channel" }),
@@ -384,24 +133,24 @@ describe("ingress invite HTTP routes", () => {
384
133
  expect(body.error).toContain("sourceChannel");
385
134
  });
386
135
 
387
- test("GET /v1/ingress/invites — lists invites", async () => {
136
+ test("GET /v1/contacts/invites — lists invites", async () => {
388
137
  // Create two invites
389
138
  await handleCreateInvite(
390
- new Request("http://localhost/v1/ingress/invites", {
139
+ new Request("http://localhost/v1/contacts/invites", {
391
140
  method: "POST",
392
141
  headers: { "Content-Type": "application/json" },
393
142
  body: JSON.stringify({ sourceChannel: "telegram" }),
394
143
  }),
395
144
  );
396
145
  await handleCreateInvite(
397
- new Request("http://localhost/v1/ingress/invites", {
146
+ new Request("http://localhost/v1/contacts/invites", {
398
147
  method: "POST",
399
148
  headers: { "Content-Type": "application/json" },
400
149
  body: JSON.stringify({ sourceChannel: "telegram" }),
401
150
  }),
402
151
  );
403
152
 
404
- const url = new URL("http://localhost/v1/ingress/invites");
153
+ const url = new URL("http://localhost/v1/contacts/invites");
405
154
  const res = handleListInvites(url);
406
155
  const body = (await res.json()) as Record<string, unknown>;
407
156
 
@@ -411,9 +160,9 @@ describe("ingress invite HTTP routes", () => {
411
160
  expect((body.invites as unknown[]).length).toBe(2);
412
161
  });
413
162
 
414
- test("DELETE /v1/ingress/invites/:id — revokes an invite", async () => {
163
+ test("DELETE /v1/contacts/invites/:id — revokes an invite", async () => {
415
164
  const createRes = await handleCreateInvite(
416
- new Request("http://localhost/v1/ingress/invites", {
165
+ new Request("http://localhost/v1/contacts/invites", {
417
166
  method: "POST",
418
167
  headers: { "Content-Type": "application/json" },
419
168
  body: JSON.stringify({ sourceChannel: "telegram" }),
@@ -430,15 +179,15 @@ describe("ingress invite HTTP routes", () => {
430
179
  expect(invite.status).toBe("revoked");
431
180
  });
432
181
 
433
- test("DELETE /v1/ingress/invites/:id — not found returns 404", () => {
182
+ test("DELETE /v1/contacts/invites/:id — not found returns 404", () => {
434
183
  const res = handleRevokeInvite("nonexistent-id");
435
184
  expect(res.status).toBe(404);
436
185
  });
437
186
 
438
- test("POST /v1/ingress/invites/redeem — redeems an invite", async () => {
187
+ test("POST /v1/contacts/invites/redeem — redeems an invite", async () => {
439
188
  // Create an invite first
440
189
  const createRes = await handleCreateInvite(
441
- new Request("http://localhost/v1/ingress/invites", {
190
+ new Request("http://localhost/v1/contacts/invites", {
442
191
  method: "POST",
443
192
  headers: { "Content-Type": "application/json" },
444
193
  body: JSON.stringify({ sourceChannel: "telegram", maxUses: 1 }),
@@ -446,7 +195,7 @@ describe("ingress invite HTTP routes", () => {
446
195
  );
447
196
  const created = (await createRes.json()) as { invite: { token: string } };
448
197
 
449
- const req = new Request("http://localhost/v1/ingress/invites/redeem", {
198
+ const req = new Request("http://localhost/v1/contacts/invites/redeem", {
450
199
  method: "POST",
451
200
  headers: { "Content-Type": "application/json" },
452
201
  body: JSON.stringify({
@@ -467,8 +216,8 @@ describe("ingress invite HTTP routes", () => {
467
216
  expect(invite.status).toBe("redeemed");
468
217
  });
469
218
 
470
- test("POST /v1/ingress/invites/redeem — missing token returns 400", async () => {
471
- const req = new Request("http://localhost/v1/ingress/invites/redeem", {
219
+ test("POST /v1/contacts/invites/redeem — missing token returns 400", async () => {
220
+ const req = new Request("http://localhost/v1/contacts/invites/redeem", {
472
221
  method: "POST",
473
222
  headers: { "Content-Type": "application/json" },
474
223
  body: JSON.stringify({ externalUserId: "redeemer-1" }),
@@ -482,8 +231,8 @@ describe("ingress invite HTTP routes", () => {
482
231
  expect(body.error).toContain("token");
483
232
  });
484
233
 
485
- test("POST /v1/ingress/invites/redeem — invalid token returns 400", async () => {
486
- const req = new Request("http://localhost/v1/ingress/invites/redeem", {
234
+ test("POST /v1/contacts/invites/redeem — invalid token returns 400", async () => {
235
+ const req = new Request("http://localhost/v1/contacts/invites/redeem", {
487
236
  method: "POST",
488
237
  headers: { "Content-Type": "application/json" },
489
238
  body: JSON.stringify({ token: "invalid-token" }),
@@ -498,44 +247,15 @@ describe("ingress invite HTTP routes", () => {
498
247
  });
499
248
 
500
249
  // ---------------------------------------------------------------------------
501
- // IPC backward compatibility — shared logic produces same results
250
+ // Shared logic round-trip
502
251
  // ---------------------------------------------------------------------------
503
252
 
504
253
  describe("ingress service shared logic", () => {
505
254
  beforeEach(resetTables);
506
255
 
507
- test("member upsert + list round-trip through shared service", async () => {
508
- const createRes = await handleUpsertMember(
509
- new Request("http://localhost/v1/ingress/members", {
510
- method: "POST",
511
- headers: { "Content-Type": "application/json" },
512
- body: JSON.stringify({
513
- sourceChannel: "telegram",
514
- externalUserId: "user-rt",
515
- displayName: "Round Trip",
516
- policy: "allow",
517
- status: "active",
518
- }),
519
- }),
520
- );
521
- const created = (await createRes.json()) as {
522
- member: { id: string; displayName: string };
523
- };
524
- expect(created.member.displayName).toBe("Round Trip");
525
-
526
- const listRes = handleListMembers(
527
- new URL("http://localhost/v1/ingress/members"),
528
- );
529
- const listed = (await listRes.json()) as {
530
- members: Array<{ id: string; displayName: string }>;
531
- };
532
- expect(listed.members.length).toBe(1);
533
- expect(listed.members[0].id).toBe(created.member.id);
534
- });
535
-
536
256
  test("invite create + revoke round-trip through shared service", async () => {
537
257
  const createRes = await handleCreateInvite(
538
- new Request("http://localhost/v1/ingress/invites", {
258
+ new Request("http://localhost/v1/contacts/invites", {
539
259
  method: "POST",
540
260
  headers: { "Content-Type": "application/json" },
541
261
  body: JSON.stringify({ sourceChannel: "telegram" }),
@@ -562,8 +282,8 @@ describe("ingress service shared logic", () => {
562
282
  describe("voice invite HTTP routes", () => {
563
283
  beforeEach(resetTables);
564
284
 
565
- test("POST /v1/ingress/invites with sourceChannel voice — creates invite with voiceCode, stores hash only", async () => {
566
- const req = new Request("http://localhost/v1/ingress/invites", {
285
+ test("POST /v1/contacts/invites with sourceChannel voice — creates invite with voiceCode, stores hash only", async () => {
286
+ const req = new Request("http://localhost/v1/contacts/invites", {
567
287
  method: "POST",
568
288
  headers: { "Content-Type": "application/json" },
569
289
  body: JSON.stringify({
@@ -599,7 +319,7 @@ describe("voice invite HTTP routes", () => {
599
319
  });
600
320
 
601
321
  test("voice invite creation requires expectedExternalUserId", async () => {
602
- const req = new Request("http://localhost/v1/ingress/invites", {
322
+ const req = new Request("http://localhost/v1/contacts/invites", {
603
323
  method: "POST",
604
324
  headers: { "Content-Type": "application/json" },
605
325
  body: JSON.stringify({
@@ -618,7 +338,7 @@ describe("voice invite HTTP routes", () => {
618
338
  });
619
339
 
620
340
  test("voice invite creation validates E.164 format", async () => {
621
- const req = new Request("http://localhost/v1/ingress/invites", {
341
+ const req = new Request("http://localhost/v1/contacts/invites", {
622
342
  method: "POST",
623
343
  headers: { "Content-Type": "application/json" },
624
344
  body: JSON.stringify({
@@ -638,7 +358,7 @@ describe("voice invite HTTP routes", () => {
638
358
  });
639
359
 
640
360
  test("voice invite creation requires friendName", async () => {
641
- const req = new Request("http://localhost/v1/ingress/invites", {
361
+ const req = new Request("http://localhost/v1/contacts/invites", {
642
362
  method: "POST",
643
363
  headers: { "Content-Type": "application/json" },
644
364
  body: JSON.stringify({
@@ -657,7 +377,7 @@ describe("voice invite HTTP routes", () => {
657
377
  });
658
378
 
659
379
  test("voice invite creation requires guardianName", async () => {
660
- const req = new Request("http://localhost/v1/ingress/invites", {
380
+ const req = new Request("http://localhost/v1/contacts/invites", {
661
381
  method: "POST",
662
382
  headers: { "Content-Type": "application/json" },
663
383
  body: JSON.stringify({
@@ -676,7 +396,7 @@ describe("voice invite HTTP routes", () => {
676
396
  });
677
397
 
678
398
  test("voiceCodeDigits is always 6 — custom values are ignored", async () => {
679
- const req = new Request("http://localhost/v1/ingress/invites", {
399
+ const req = new Request("http://localhost/v1/contacts/invites", {
680
400
  method: "POST",
681
401
  headers: { "Content-Type": "application/json" },
682
402
  body: JSON.stringify({
@@ -699,7 +419,7 @@ describe("voice invite HTTP routes", () => {
699
419
  });
700
420
 
701
421
  test("voice invites do NOT return token in response", async () => {
702
- const req = new Request("http://localhost/v1/ingress/invites", {
422
+ const req = new Request("http://localhost/v1/contacts/invites", {
703
423
  method: "POST",
704
424
  headers: { "Content-Type": "application/json" },
705
425
  body: JSON.stringify({
@@ -720,10 +440,10 @@ describe("voice invite HTTP routes", () => {
720
440
  expect(invite.token).toBeUndefined();
721
441
  });
722
442
 
723
- test("POST /v1/ingress/invites/redeem — redeems a voice invite code via unified endpoint", async () => {
443
+ test("POST /v1/contacts/invites/redeem — redeems a voice invite code via unified endpoint", async () => {
724
444
  // Create a voice invite
725
445
  const createRes = await handleCreateInvite(
726
- new Request("http://localhost/v1/ingress/invites", {
446
+ new Request("http://localhost/v1/contacts/invites", {
727
447
  method: "POST",
728
448
  headers: { "Content-Type": "application/json" },
729
449
  body: JSON.stringify({
@@ -741,7 +461,7 @@ describe("voice invite HTTP routes", () => {
741
461
 
742
462
  // Redeem the voice code via the unified /redeem endpoint
743
463
  const redeemReq = new Request(
744
- "http://localhost/v1/ingress/invites/redeem",
464
+ "http://localhost/v1/contacts/invites/redeem",
745
465
  {
746
466
  method: "POST",
747
467
  headers: { "Content-Type": "application/json" },
@@ -762,8 +482,8 @@ describe("voice invite HTTP routes", () => {
762
482
  expect(typeof body.inviteId).toBe("string");
763
483
  });
764
484
 
765
- test("POST /v1/ingress/invites/redeem — voice code missing fields returns 400", async () => {
766
- const req = new Request("http://localhost/v1/ingress/invites/redeem", {
485
+ test("POST /v1/contacts/invites/redeem — voice code missing fields returns 400", async () => {
486
+ const req = new Request("http://localhost/v1/contacts/invites/redeem", {
767
487
  method: "POST",
768
488
  headers: { "Content-Type": "application/json" },
769
489
  body: JSON.stringify({ callerExternalUserId: "+15551234567" }),
@@ -777,10 +497,10 @@ describe("voice invite HTTP routes", () => {
777
497
  expect(body.ok).toBe(false);
778
498
  });
779
499
 
780
- test("POST /v1/ingress/invites/redeem — wrong voice code returns 400", async () => {
500
+ test("POST /v1/contacts/invites/redeem — wrong voice code returns 400", async () => {
781
501
  // Create a voice invite
782
502
  await handleCreateInvite(
783
- new Request("http://localhost/v1/ingress/invites", {
503
+ new Request("http://localhost/v1/contacts/invites", {
784
504
  method: "POST",
785
505
  headers: { "Content-Type": "application/json" },
786
506
  body: JSON.stringify({
@@ -793,7 +513,7 @@ describe("voice invite HTTP routes", () => {
793
513
  }),
794
514
  );
795
515
 
796
- const req = new Request("http://localhost/v1/ingress/invites/redeem", {
516
+ const req = new Request("http://localhost/v1/contacts/invites/redeem", {
797
517
  method: "POST",
798
518
  headers: { "Content-Type": "application/json" },
799
519
  body: JSON.stringify({
@@ -595,25 +595,14 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
595
595
  cursorInTextField: true,
596
596
  },
597
597
  },
598
- ingress_invite: {
599
- type: "ingress_invite",
598
+ contacts_invite: {
599
+ type: "contacts_invite",
600
600
  action: "create",
601
601
  sourceChannel: "telegram",
602
602
  note: "Test invite",
603
603
  maxUses: 5,
604
604
  expiresInMs: 86400000,
605
605
  },
606
- ingress_member: {
607
- type: "ingress_member",
608
- action: "upsert",
609
- sourceChannel: "telegram",
610
- externalUserId: "user-123",
611
- externalChatId: "chat-456",
612
- displayName: "Test User",
613
- username: "testuser",
614
- policy: "allow",
615
- status: "active",
616
- },
617
606
  assistant_inbox_escalation: {
618
607
  type: "assistant_inbox_escalation",
619
608
  action: "list",
@@ -1871,8 +1860,8 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1871
1860
  mode: "dictation",
1872
1861
  actionPlan: undefined,
1873
1862
  },
1874
- ingress_invite_response: {
1875
- type: "ingress_invite_response",
1863
+ contacts_invite_response: {
1864
+ type: "contacts_invite_response",
1876
1865
  success: true,
1877
1866
  invite: {
1878
1867
  id: "inv-001",
@@ -1887,22 +1876,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1887
1876
  createdAt: 1700000000,
1888
1877
  },
1889
1878
  },
1890
- ingress_member_response: {
1891
- type: "ingress_member_response",
1892
- success: true,
1893
- member: {
1894
- id: "mem-001",
1895
- sourceChannel: "telegram",
1896
- externalUserId: "user-123",
1897
- externalChatId: "chat-456",
1898
- displayName: "Test User",
1899
- username: "testuser",
1900
- status: "active",
1901
- policy: "allow",
1902
- lastSeenAt: 1700000000,
1903
- createdAt: 1700000000,
1904
- },
1905
- },
1906
1879
  assistant_inbox_escalation_response: {
1907
1880
  type: "assistant_inbox_escalation_response",
1908
1881
  success: true,