@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.
- package/ARCHITECTURE.md +39 -37
- package/README.md +5 -6
- package/docs/runbook-trusted-contacts.md +79 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/calls/relay-server.ts +39 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +42 -35
- package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-skills/weather/TOOLS.json +4 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/contacts/contact-store.ts +195 -4
- package/src/contacts/types.ts +26 -0
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/guardian-actions.ts +20 -87
- package/src/daemon/handlers/sessions.ts +0 -1
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +1 -0
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- package/src/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +12 -0
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +127 -0
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +20 -49
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +196 -28
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +76 -0
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- /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(), "
|
|
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
|
-
|
|
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/
|
|
311
|
-
const req = new Request("http://localhost/v1/
|
|
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/
|
|
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/
|
|
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/
|
|
373
|
-
const req = new Request("http://localhost/v1/
|
|
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/
|
|
136
|
+
test("GET /v1/contacts/invites — lists invites", async () => {
|
|
388
137
|
// Create two invites
|
|
389
138
|
await handleCreateInvite(
|
|
390
|
-
new Request("http://localhost/v1/
|
|
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/
|
|
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/
|
|
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/
|
|
163
|
+
test("DELETE /v1/contacts/invites/:id — revokes an invite", async () => {
|
|
415
164
|
const createRes = await handleCreateInvite(
|
|
416
|
-
new Request("http://localhost/v1/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
471
|
-
const req = new Request("http://localhost/v1/
|
|
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/
|
|
486
|
-
const req = new Request("http://localhost/v1/
|
|
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
|
-
//
|
|
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/
|
|
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/
|
|
566
|
-
const req = new Request("http://localhost/v1/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
766
|
-
const req = new Request("http://localhost/v1/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
599
|
-
type: "
|
|
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
|
-
|
|
1875
|
-
type: "
|
|
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,
|