@vellumai/assistant 0.4.37 → 0.4.41
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 +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
|
@@ -52,25 +52,32 @@ mock.module("../util/logger.js", () => ({
|
|
|
52
52
|
// Mock secure key storage
|
|
53
53
|
let secureKeyStore: Record<string, string> = {};
|
|
54
54
|
|
|
55
|
-
mock.module("../security/secure-keys.js", () =>
|
|
56
|
-
|
|
57
|
-
setSecureKey: (account: string, value: string) => {
|
|
55
|
+
mock.module("../security/secure-keys.js", () => {
|
|
56
|
+
const syncSet = (account: string, value: string) => {
|
|
58
57
|
secureKeyStore[account] = value;
|
|
59
58
|
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
59
|
+
};
|
|
60
|
+
const syncDelete = (account: string) => {
|
|
62
61
|
if (account in secureKeyStore) {
|
|
63
62
|
delete secureKeyStore[account];
|
|
64
|
-
return "deleted";
|
|
63
|
+
return "deleted" as const;
|
|
65
64
|
}
|
|
66
|
-
return "not-found";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
return "not-found" as const;
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
|
|
69
|
+
setSecureKey: syncSet,
|
|
70
|
+
deleteSecureKey: syncDelete,
|
|
71
|
+
setSecureKeyAsync: async (account: string, value: string) =>
|
|
72
|
+
syncSet(account, value),
|
|
73
|
+
deleteSecureKeyAsync: async (account: string) => syncDelete(account),
|
|
74
|
+
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
75
|
+
getBackendType: () => "encrypted",
|
|
76
|
+
isDowngradedFromKeychain: () => false,
|
|
77
|
+
_resetBackend: () => {},
|
|
78
|
+
_setBackend: () => {},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
74
81
|
|
|
75
82
|
// Mock credential metadata store
|
|
76
83
|
let credentialMetadataStore: Array<{
|
|
@@ -245,7 +252,7 @@ describe("Slack channel config handler", () => {
|
|
|
245
252
|
expect(result.error).toContain("invalid_auth");
|
|
246
253
|
});
|
|
247
254
|
|
|
248
|
-
test("DELETE clears credentials", () => {
|
|
255
|
+
test("DELETE clears credentials", async () => {
|
|
249
256
|
secureKeyStore["credential:slack_channel:bot_token"] = "xoxb-test";
|
|
250
257
|
secureKeyStore["credential:slack_channel:app_token"] = "xapp-test";
|
|
251
258
|
credentialMetadataStore.push({
|
|
@@ -257,7 +264,7 @@ describe("Slack channel config handler", () => {
|
|
|
257
264
|
field: "app_token",
|
|
258
265
|
});
|
|
259
266
|
|
|
260
|
-
const result = clearSlackChannelConfig();
|
|
267
|
+
const result = await clearSlackChannelConfig();
|
|
261
268
|
expect(result.success).toBe(true);
|
|
262
269
|
expect(result.hasBotToken).toBe(false);
|
|
263
270
|
expect(result.hasAppToken).toBe(false);
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mocks — must be declared before any imports that pull in mocked modules
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const secureKeyValues = new Map<string, string>();
|
|
8
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
9
|
+
getSecureKey: (key: string) => secureKeyValues.get(key),
|
|
10
|
+
setSecureKeyAsync: async () => {},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
let listConversationsResult: unknown = { ok: true, channels: [] };
|
|
14
|
+
let postMessageResult: unknown = {
|
|
15
|
+
ok: true,
|
|
16
|
+
ts: "1234567890.123456",
|
|
17
|
+
channel: "C123",
|
|
18
|
+
message: { ts: "1234567890.123456", text: "", type: "message" },
|
|
19
|
+
};
|
|
20
|
+
let userInfoResults: Map<string, unknown> = new Map();
|
|
21
|
+
|
|
22
|
+
mock.module("../messaging/providers/slack/client.js", () => ({
|
|
23
|
+
listConversations: async () => listConversationsResult,
|
|
24
|
+
postMessage: async (
|
|
25
|
+
_token: string,
|
|
26
|
+
_channel: string,
|
|
27
|
+
_text: string,
|
|
28
|
+
_opts?: unknown,
|
|
29
|
+
) => postMessageResult,
|
|
30
|
+
userInfo: async (_token: string, userId: string) => {
|
|
31
|
+
const result = userInfoResults.get(userId);
|
|
32
|
+
if (result) return result;
|
|
33
|
+
throw new Error(`User not found: ${userId}`);
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
let appStoreResult: unknown = null;
|
|
38
|
+
mock.module("../memory/app-store.js", () => ({
|
|
39
|
+
getApp: (_id: string) => appStoreResult,
|
|
40
|
+
getAppsDir: () => "/tmp/apps",
|
|
41
|
+
isMultifileApp: () => false,
|
|
42
|
+
listApps: () => [],
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module("../util/logger.js", () => ({
|
|
46
|
+
getLogger: () => ({
|
|
47
|
+
info: () => {},
|
|
48
|
+
warn: () => {},
|
|
49
|
+
error: () => {},
|
|
50
|
+
debug: () => {},
|
|
51
|
+
trace: () => {},
|
|
52
|
+
fatal: () => {},
|
|
53
|
+
child: () => ({
|
|
54
|
+
info: () => {},
|
|
55
|
+
warn: () => {},
|
|
56
|
+
error: () => {},
|
|
57
|
+
debug: () => {},
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Import under test (after mocks)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const { handleListSlackChannels, handleShareToSlackChannel } =
|
|
67
|
+
await import("../runtime/routes/slack-share-routes.js");
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function makeRequest(body: unknown): Request {
|
|
74
|
+
return new Request("http://localhost/v1/slack/share", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Tests
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
secureKeyValues.clear();
|
|
87
|
+
listConversationsResult = { ok: true, channels: [] };
|
|
88
|
+
userInfoResults = new Map();
|
|
89
|
+
appStoreResult = null;
|
|
90
|
+
postMessageResult = {
|
|
91
|
+
ok: true,
|
|
92
|
+
ts: "1234567890.123456",
|
|
93
|
+
channel: "C123",
|
|
94
|
+
message: { ts: "1234567890.123456", text: "", type: "message" },
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("handleListSlackChannels", () => {
|
|
99
|
+
test("returns 503 when no token is configured", async () => {
|
|
100
|
+
const res = await handleListSlackChannels();
|
|
101
|
+
expect(res.status).toBe(503);
|
|
102
|
+
const json = (await res.json()) as { error: { code: string } };
|
|
103
|
+
expect(json.error.code).toBe("SERVICE_UNAVAILABLE");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("returns channels sorted by type then name", async () => {
|
|
107
|
+
secureKeyValues.set(
|
|
108
|
+
"credential:integration:slack:access_token",
|
|
109
|
+
"xoxb-test",
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
listConversationsResult = {
|
|
113
|
+
ok: true,
|
|
114
|
+
channels: [
|
|
115
|
+
{
|
|
116
|
+
id: "D1",
|
|
117
|
+
name: undefined,
|
|
118
|
+
is_im: true,
|
|
119
|
+
user: "U1",
|
|
120
|
+
is_private: true,
|
|
121
|
+
},
|
|
122
|
+
{ id: "C2", name: "beta-channel", is_channel: true },
|
|
123
|
+
{ id: "C1", name: "alpha-channel", is_channel: true },
|
|
124
|
+
{ id: "G1", name: "group-chat", is_mpim: true, is_private: true },
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
userInfoResults.set("U1", {
|
|
129
|
+
ok: true,
|
|
130
|
+
user: {
|
|
131
|
+
id: "U1",
|
|
132
|
+
name: "alice",
|
|
133
|
+
profile: { display_name: "Alice Smith" },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const res = await handleListSlackChannels();
|
|
138
|
+
expect(res.status).toBe(200);
|
|
139
|
+
const json = (await res.json()) as {
|
|
140
|
+
channels: Array<{
|
|
141
|
+
id: string;
|
|
142
|
+
name: string;
|
|
143
|
+
type: string;
|
|
144
|
+
isPrivate: boolean;
|
|
145
|
+
}>;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(json.channels).toHaveLength(4);
|
|
149
|
+
// Channels first (alphabetical)
|
|
150
|
+
expect(json.channels[0]).toEqual({
|
|
151
|
+
id: "C1",
|
|
152
|
+
name: "alpha-channel",
|
|
153
|
+
type: "channel",
|
|
154
|
+
isPrivate: false,
|
|
155
|
+
});
|
|
156
|
+
expect(json.channels[1]).toEqual({
|
|
157
|
+
id: "C2",
|
|
158
|
+
name: "beta-channel",
|
|
159
|
+
type: "channel",
|
|
160
|
+
isPrivate: false,
|
|
161
|
+
});
|
|
162
|
+
// Groups
|
|
163
|
+
expect(json.channels[2]).toEqual({
|
|
164
|
+
id: "G1",
|
|
165
|
+
name: "group-chat",
|
|
166
|
+
type: "group",
|
|
167
|
+
isPrivate: true,
|
|
168
|
+
});
|
|
169
|
+
// DMs (name resolved from userInfo)
|
|
170
|
+
expect(json.channels[3]).toEqual({
|
|
171
|
+
id: "D1",
|
|
172
|
+
name: "Alice Smith",
|
|
173
|
+
type: "dm",
|
|
174
|
+
isPrivate: true,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("falls back to legacy bot token", async () => {
|
|
179
|
+
secureKeyValues.set("credential:slack_channel:bot_token", "xoxb-legacy");
|
|
180
|
+
|
|
181
|
+
listConversationsResult = { ok: true, channels: [] };
|
|
182
|
+
|
|
183
|
+
const res = await handleListSlackChannels();
|
|
184
|
+
expect(res.status).toBe(200);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("handleShareToSlackChannel", () => {
|
|
189
|
+
test("returns 503 when no token is configured", async () => {
|
|
190
|
+
const req = makeRequest({ appId: "app1", channelId: "C1" });
|
|
191
|
+
const res = await handleShareToSlackChannel(req);
|
|
192
|
+
expect(res.status).toBe(503);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("returns 400 for malformed JSON", async () => {
|
|
196
|
+
secureKeyValues.set(
|
|
197
|
+
"credential:integration:slack:access_token",
|
|
198
|
+
"xoxb-test",
|
|
199
|
+
);
|
|
200
|
+
const req = new Request("http://localhost/v1/slack/share", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: "not json",
|
|
204
|
+
});
|
|
205
|
+
const res = await handleShareToSlackChannel(req);
|
|
206
|
+
expect(res.status).toBe(400);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("returns 400 when missing required fields", async () => {
|
|
210
|
+
secureKeyValues.set(
|
|
211
|
+
"credential:integration:slack:access_token",
|
|
212
|
+
"xoxb-test",
|
|
213
|
+
);
|
|
214
|
+
const req = makeRequest({ appId: "app1" });
|
|
215
|
+
const res = await handleShareToSlackChannel(req);
|
|
216
|
+
expect(res.status).toBe(400);
|
|
217
|
+
const json = (await res.json()) as { error: { message: string } };
|
|
218
|
+
expect(json.error.message).toContain("Missing required fields");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("returns 404 when app not found", async () => {
|
|
222
|
+
secureKeyValues.set(
|
|
223
|
+
"credential:integration:slack:access_token",
|
|
224
|
+
"xoxb-test",
|
|
225
|
+
);
|
|
226
|
+
appStoreResult = null;
|
|
227
|
+
const req = makeRequest({ appId: "missing-app", channelId: "C1" });
|
|
228
|
+
const res = await handleShareToSlackChannel(req);
|
|
229
|
+
expect(res.status).toBe(404);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("posts message and returns success", async () => {
|
|
233
|
+
secureKeyValues.set(
|
|
234
|
+
"credential:integration:slack:access_token",
|
|
235
|
+
"xoxb-test",
|
|
236
|
+
);
|
|
237
|
+
appStoreResult = {
|
|
238
|
+
id: "app1",
|
|
239
|
+
name: "My App",
|
|
240
|
+
description: "A great app",
|
|
241
|
+
htmlDefinition: "<div></div>",
|
|
242
|
+
schemaJson: "{}",
|
|
243
|
+
createdAt: 0,
|
|
244
|
+
updatedAt: 0,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const req = makeRequest({
|
|
248
|
+
appId: "app1",
|
|
249
|
+
channelId: "C123",
|
|
250
|
+
message: "Check this out!",
|
|
251
|
+
});
|
|
252
|
+
const res = await handleShareToSlackChannel(req);
|
|
253
|
+
expect(res.status).toBe(200);
|
|
254
|
+
const json = (await res.json()) as {
|
|
255
|
+
ok: boolean;
|
|
256
|
+
ts: string;
|
|
257
|
+
channel: string;
|
|
258
|
+
};
|
|
259
|
+
expect(json.ok).toBe(true);
|
|
260
|
+
expect(json.ts).toBe("1234567890.123456");
|
|
261
|
+
expect(json.channel).toBe("C123");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -21,11 +21,13 @@ let secureKeys: Record<string, string | undefined> = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
let configState: {
|
|
24
|
+
twilio?: { accountSid?: string };
|
|
24
25
|
sms?: {
|
|
25
26
|
phoneNumber?: string;
|
|
26
27
|
assistantPhoneNumbers?: Record<string, string>;
|
|
27
28
|
};
|
|
28
29
|
} = {
|
|
30
|
+
twilio: { accountSid: "AC1234567890" },
|
|
29
31
|
sms: {},
|
|
30
32
|
};
|
|
31
33
|
|
|
@@ -71,7 +73,7 @@ describe("smsMessagingProvider", () => {
|
|
|
71
73
|
"credential:twilio:account_sid": "AC1234567890",
|
|
72
74
|
"credential:twilio:auth_token": "auth-token",
|
|
73
75
|
};
|
|
74
|
-
configState = { sms: {} };
|
|
76
|
+
configState = { twilio: { accountSid: "AC1234567890" }, sms: {} };
|
|
75
77
|
delete process.env.TWILIO_PHONE_NUMBER;
|
|
76
78
|
delete process.env.GATEWAY_INTERNAL_BASE_URL;
|
|
77
79
|
delete process.env.GATEWAY_PORT;
|
|
@@ -91,7 +91,7 @@ import { getResolver } from "../approvals/guardian-request-resolvers.js";
|
|
|
91
91
|
import { findContactChannel } from "../contacts/contact-store.js";
|
|
92
92
|
import {
|
|
93
93
|
createGuardianBinding,
|
|
94
|
-
|
|
94
|
+
upsertContactChannel,
|
|
95
95
|
} from "../contacts/contacts-write.js";
|
|
96
96
|
import { createApprovalRequest } from "../memory/channel-guardian-store.js";
|
|
97
97
|
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
@@ -174,7 +174,7 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
174
174
|
guardianPrincipalId: "guardian-user-789",
|
|
175
175
|
verifiedVia: "test",
|
|
176
176
|
});
|
|
177
|
-
|
|
177
|
+
upsertContactChannel({
|
|
178
178
|
sourceChannel: "telegram",
|
|
179
179
|
externalUserId: "guardian-user-789",
|
|
180
180
|
externalChatId: "guardian-chat-789",
|
|
@@ -253,7 +253,7 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
253
253
|
guardianPrincipalId: "guardian-user-789",
|
|
254
254
|
verifiedVia: "test",
|
|
255
255
|
});
|
|
256
|
-
|
|
256
|
+
upsertContactChannel({
|
|
257
257
|
sourceChannel: "telegram",
|
|
258
258
|
externalUserId: "guardian-user-789",
|
|
259
259
|
externalChatId: "guardian-chat-789",
|
|
@@ -328,7 +328,7 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
328
328
|
guardianPrincipalId: "guardian-user-789",
|
|
329
329
|
verifiedVia: "test",
|
|
330
330
|
});
|
|
331
|
-
|
|
331
|
+
upsertContactChannel({
|
|
332
332
|
sourceChannel: "telegram",
|
|
333
333
|
externalUserId: "guardian-user-789",
|
|
334
334
|
externalChatId: "guardian-chat-789",
|
|
@@ -448,7 +448,7 @@ describe("trusted contact activated notification signal", () => {
|
|
|
448
448
|
verifiedVia: "test",
|
|
449
449
|
});
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
upsertContactChannel({
|
|
452
452
|
sourceChannel: "telegram",
|
|
453
453
|
externalUserId: "requester-user-456",
|
|
454
454
|
externalChatId: "chat-123",
|
|
@@ -509,7 +509,7 @@ describe("trusted contact activated notification signal", () => {
|
|
|
509
509
|
test("voice access_request resolver has registered handler for access_request kind", () => {
|
|
510
510
|
// The access_request resolver is registered during module load. When the
|
|
511
511
|
// source channel is 'voice', it should directly activate the member via
|
|
512
|
-
//
|
|
512
|
+
// upsertContactChannel (no verification session). This test validates the resolver
|
|
513
513
|
// is registered and accessible.
|
|
514
514
|
const resolver = getResolver("access_request");
|
|
515
515
|
expect(resolver).toBeDefined();
|
|
@@ -549,7 +549,7 @@ describe("trusted contact activated notification signal", () => {
|
|
|
549
549
|
);
|
|
550
550
|
expect(activatedSignals.length).toBe(1);
|
|
551
551
|
|
|
552
|
-
// Verify the member was already persisted (the signal fires after
|
|
552
|
+
// Verify the member was already persisted (the signal fires after upsertContactChannel)
|
|
553
553
|
const result = findContactChannel({
|
|
554
554
|
channelType: "telegram",
|
|
555
555
|
externalUserId: "requester-user-456",
|
|
@@ -98,7 +98,7 @@ mock.module("../runtime/approval-message-composer.js", () => ({
|
|
|
98
98
|
import { findContactChannel } from "../contacts/contact-store.js";
|
|
99
99
|
import {
|
|
100
100
|
createGuardianBinding,
|
|
101
|
-
|
|
101
|
+
upsertContactChannel,
|
|
102
102
|
} from "../contacts/contacts-write.js";
|
|
103
103
|
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
104
104
|
import {
|
|
@@ -279,7 +279,7 @@ for (const config of CHANNEL_CONFIGS) {
|
|
|
279
279
|
expect(challengeResult.verificationType).toBe("trusted_contact");
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
|
|
282
|
+
upsertContactChannel({
|
|
283
283
|
sourceChannel: config.channel,
|
|
284
284
|
externalUserId: config.senderExternalUserId,
|
|
285
285
|
externalChatId: config.externalChatId,
|
|
@@ -302,7 +302,7 @@ for (const config of CHANNEL_CONFIGS) {
|
|
|
302
302
|
|
|
303
303
|
test("no cross-channel leakage between member records", () => {
|
|
304
304
|
// Create a member for this channel
|
|
305
|
-
|
|
305
|
+
upsertContactChannel({
|
|
306
306
|
sourceChannel: config.channel,
|
|
307
307
|
externalUserId: config.senderExternalUserId,
|
|
308
308
|
externalChatId: config.externalChatId,
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
import {
|
|
48
48
|
createGuardianBinding,
|
|
49
49
|
revokeMember,
|
|
50
|
-
|
|
50
|
+
upsertContactChannel,
|
|
51
51
|
} from "../contacts/contacts-write.js";
|
|
52
52
|
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
53
53
|
import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
|
|
@@ -115,7 +115,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// Simulate the member upsert that inbound-message-handler performs on success
|
|
118
|
-
|
|
118
|
+
upsertContactChannel({
|
|
119
119
|
sourceChannel: "telegram",
|
|
120
120
|
externalUserId: "requester-user-123",
|
|
121
121
|
externalChatId: "requester-chat-123",
|
|
@@ -141,7 +141,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
test("resolveActorTrust surfaces member displayName when sender displayName is missing", () => {
|
|
144
|
-
|
|
144
|
+
upsertContactChannel({
|
|
145
145
|
sourceChannel: "telegram",
|
|
146
146
|
externalUserId: "requester-user-jeff",
|
|
147
147
|
externalChatId: "requester-chat-jeff",
|
|
@@ -169,7 +169,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
test("resolveActorTrust prioritizes member displayName over sender displayName", () => {
|
|
172
|
-
|
|
172
|
+
upsertContactChannel({
|
|
173
173
|
sourceChannel: "telegram",
|
|
174
174
|
externalUserId: "requester-user-jeff-priority",
|
|
175
175
|
externalChatId: "requester-chat-jeff-priority",
|
|
@@ -201,7 +201,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
201
201
|
test("resolveActorTrust falls back to sender metadata when member record matches chat but not sender (group chat)", () => {
|
|
202
202
|
// Simulate a group chat: member record exists for a different user who
|
|
203
203
|
// shares the same externalChatId (e.g., Telegram group).
|
|
204
|
-
|
|
204
|
+
upsertContactChannel({
|
|
205
205
|
sourceChannel: "telegram",
|
|
206
206
|
externalUserId: "other-user-in-group",
|
|
207
207
|
externalChatId: "shared-group-chat",
|
|
@@ -251,7 +251,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
251
251
|
);
|
|
252
252
|
|
|
253
253
|
// Simulate member upsert on verification success
|
|
254
|
-
|
|
254
|
+
upsertContactChannel({
|
|
255
255
|
sourceChannel: "telegram",
|
|
256
256
|
externalUserId: "requester-user-456",
|
|
257
257
|
externalChatId: "requester-chat-456",
|
|
@@ -289,7 +289,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
289
289
|
"chat-cross-test",
|
|
290
290
|
);
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
upsertContactChannel({
|
|
293
293
|
sourceChannel: "telegram",
|
|
294
294
|
externalUserId: "user-cross-test",
|
|
295
295
|
externalChatId: "chat-cross-test",
|
|
@@ -315,7 +315,7 @@ describe("trusted contact verification → member activation", () => {
|
|
|
315
315
|
|
|
316
316
|
test("re-verification of previously revoked member reactivates them", () => {
|
|
317
317
|
// Create and activate a member
|
|
318
|
-
const member =
|
|
318
|
+
const member = upsertContactChannel({
|
|
319
319
|
sourceChannel: "telegram",
|
|
320
320
|
externalUserId: "user-revoked",
|
|
321
321
|
externalChatId: "chat-revoked",
|
|
@@ -359,8 +359,8 @@ describe("trusted contact verification → member activation", () => {
|
|
|
359
359
|
expect(result.verificationType).toBe("trusted_contact");
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
//
|
|
363
|
-
|
|
362
|
+
// upsertContactChannel reactivates the existing record
|
|
363
|
+
upsertContactChannel({
|
|
364
364
|
sourceChannel: "telegram",
|
|
365
365
|
externalUserId: "user-revoked",
|
|
366
366
|
externalChatId: "chat-revoked",
|
|
@@ -3,7 +3,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
3
3
|
// ── Mocks (must come before source imports) ──────────────────────────
|
|
4
4
|
|
|
5
5
|
let mockSecureKeys: Record<string, string | null> = {};
|
|
6
|
-
let mockPhoneNumberEnv: string | undefined;
|
|
7
6
|
let mockLoadConfigResult: Record<string, unknown> = {};
|
|
8
7
|
|
|
9
8
|
mock.module("../util/logger.js", () => ({
|
|
@@ -17,11 +16,6 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
17
16
|
getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
|
|
18
17
|
}));
|
|
19
18
|
|
|
20
|
-
mock.module("../config/env.js", () => ({
|
|
21
|
-
isHttpAuthDisabled: () => true,
|
|
22
|
-
getTwilioPhoneNumberEnv: () => mockPhoneNumberEnv,
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
19
|
mock.module("../config/loader.js", () => ({
|
|
26
20
|
loadConfig: () => mockLoadConfigResult,
|
|
27
21
|
}));
|
|
@@ -36,12 +30,13 @@ import { getTwilioConfig } from "../calls/twilio-config.js";
|
|
|
36
30
|
describe("twilio-config", () => {
|
|
37
31
|
beforeEach(() => {
|
|
38
32
|
mockSecureKeys = {
|
|
39
|
-
"credential:twilio:account_sid": "AC_test_sid",
|
|
40
33
|
"credential:twilio:auth_token": "test_auth_token",
|
|
41
34
|
};
|
|
42
|
-
mockPhoneNumberEnv = undefined;
|
|
43
35
|
mockLoadConfigResult = {
|
|
44
|
-
|
|
36
|
+
twilio: {
|
|
37
|
+
accountSid: "AC_test_sid",
|
|
38
|
+
phoneNumber: "+15551234567",
|
|
39
|
+
},
|
|
45
40
|
};
|
|
46
41
|
});
|
|
47
42
|
|
|
@@ -55,7 +50,9 @@ describe("twilio-config", () => {
|
|
|
55
50
|
});
|
|
56
51
|
|
|
57
52
|
test("throws ConfigError when account SID is missing", () => {
|
|
58
|
-
|
|
53
|
+
mockLoadConfigResult = {
|
|
54
|
+
twilio: { accountSid: "", phoneNumber: "+15551234567" },
|
|
55
|
+
};
|
|
59
56
|
expect(() => getTwilioConfig()).toThrow(
|
|
60
57
|
/Twilio credentials not configured/,
|
|
61
58
|
);
|
|
@@ -69,36 +66,18 @@ describe("twilio-config", () => {
|
|
|
69
66
|
});
|
|
70
67
|
|
|
71
68
|
test("throws ConfigError when phone number is missing", () => {
|
|
72
|
-
mockLoadConfigResult = {
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
mockLoadConfigResult = {
|
|
70
|
+
twilio: { accountSid: "AC_test_sid", phoneNumber: "" },
|
|
71
|
+
};
|
|
75
72
|
expect(() => getTwilioConfig()).toThrow(
|
|
76
73
|
/Twilio phone number not configured/,
|
|
77
74
|
);
|
|
78
75
|
});
|
|
79
76
|
|
|
80
|
-
test("
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
test("falls back to secure key for phone number", () => {
|
|
87
|
-
mockLoadConfigResult = { sms: {} };
|
|
88
|
-
mockPhoneNumberEnv = undefined;
|
|
89
|
-
mockSecureKeys["credential:twilio:phone_number"] = "+15558888888";
|
|
90
|
-
const config = getTwilioConfig();
|
|
91
|
-
expect(config.phoneNumber).toBe("+15558888888");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("returns global phone number when assistantPhoneNumbers mapping exists", () => {
|
|
95
|
-
mockLoadConfigResult = {
|
|
96
|
-
sms: {
|
|
97
|
-
phoneNumber: "+15551234567",
|
|
98
|
-
assistantPhoneNumbers: { "ast-1": "+15557777777" },
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
const config = getTwilioConfig();
|
|
102
|
-
expect(config.phoneNumber).toBe("+15551234567");
|
|
77
|
+
test("throws ConfigError when twilio config section is absent", () => {
|
|
78
|
+
mockLoadConfigResult = {};
|
|
79
|
+
expect(() => getTwilioConfig()).toThrow(
|
|
80
|
+
/Twilio credentials not configured/,
|
|
81
|
+
);
|
|
103
82
|
});
|
|
104
83
|
});
|
|
@@ -33,6 +33,10 @@ mock.module("../util/logger.js", () => ({
|
|
|
33
33
|
let mockAuthToken: string | undefined = "test-auth-token-secret";
|
|
34
34
|
let mockAccountSid: string | undefined = "AC_test_account";
|
|
35
35
|
|
|
36
|
+
mock.module("../config/loader.js", () => ({
|
|
37
|
+
loadConfig: () => ({ twilio: { accountSid: mockAccountSid } }),
|
|
38
|
+
}));
|
|
39
|
+
|
|
36
40
|
mock.module("../security/secure-keys.js", () => ({
|
|
37
41
|
getSecureKey: (account: string) => {
|
|
38
42
|
if (account === "credential:twilio:auth_token") return mockAuthToken;
|