@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
|
@@ -10,18 +10,49 @@ const testDir = mkdtempSync(join(tmpdir(), "handlers-twitter-cfg-test-"));
|
|
|
10
10
|
let rawConfigStore: Record<string, unknown> = {};
|
|
11
11
|
const saveRawConfigCalls: Record<string, unknown>[] = [];
|
|
12
12
|
|
|
13
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
14
|
+
const keys = path.split(".");
|
|
15
|
+
let current: unknown = obj;
|
|
16
|
+
for (const key of keys) {
|
|
17
|
+
if (current == null || typeof current !== "object") {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
current = (current as Record<string, unknown>)[key];
|
|
21
|
+
}
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setNestedValue(
|
|
26
|
+
obj: Record<string, unknown>,
|
|
27
|
+
path: string,
|
|
28
|
+
value: unknown,
|
|
29
|
+
): void {
|
|
30
|
+
const keys = path.split(".");
|
|
31
|
+
let current = obj;
|
|
32
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
33
|
+
const key = keys[i]!;
|
|
34
|
+
if (current[key] == null || typeof current[key] !== "object") {
|
|
35
|
+
current[key] = {};
|
|
36
|
+
}
|
|
37
|
+
current = current[key] as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
current[keys[keys.length - 1]!] = value;
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
mock.module("../config/loader.js", () => ({
|
|
14
43
|
getConfig: () => ({
|
|
15
44
|
ui: {},
|
|
16
45
|
}),
|
|
17
46
|
loadConfig: () => ({}),
|
|
18
|
-
loadRawConfig: () => (
|
|
47
|
+
loadRawConfig: () => structuredClone(rawConfigStore),
|
|
19
48
|
saveRawConfig: (cfg: Record<string, unknown>) => {
|
|
20
49
|
saveRawConfigCalls.push(cfg);
|
|
21
|
-
rawConfigStore =
|
|
50
|
+
rawConfigStore = structuredClone(cfg);
|
|
22
51
|
},
|
|
23
52
|
saveConfig: () => {},
|
|
24
53
|
invalidateConfigCache: () => {},
|
|
54
|
+
getNestedValue,
|
|
55
|
+
setNestedValue,
|
|
25
56
|
}));
|
|
26
57
|
|
|
27
58
|
mock.module("../util/platform.js", () => ({
|
|
@@ -61,20 +92,27 @@ let secureKeyStore: Record<string, string> = {};
|
|
|
61
92
|
let setSecureKeyOverride: ((account: string, value: string) => boolean) | null =
|
|
62
93
|
null;
|
|
63
94
|
|
|
95
|
+
function syncSet(account: string, value: string): boolean {
|
|
96
|
+
if (setSecureKeyOverride) return setSecureKeyOverride(account, value);
|
|
97
|
+
secureKeyStore[account] = value;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function syncDelete(account: string): "deleted" | "not-found" {
|
|
102
|
+
if (account in secureKeyStore) {
|
|
103
|
+
delete secureKeyStore[account];
|
|
104
|
+
return "deleted";
|
|
105
|
+
}
|
|
106
|
+
return "not-found";
|
|
107
|
+
}
|
|
108
|
+
|
|
64
109
|
mock.module("../security/secure-keys.js", () => ({
|
|
65
110
|
getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
|
|
66
|
-
setSecureKey:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
deleteSecureKey: (account: string) => {
|
|
72
|
-
if (account in secureKeyStore) {
|
|
73
|
-
delete secureKeyStore[account];
|
|
74
|
-
return "deleted";
|
|
75
|
-
}
|
|
76
|
-
return "not-found";
|
|
77
|
-
},
|
|
111
|
+
setSecureKey: syncSet,
|
|
112
|
+
deleteSecureKey: syncDelete,
|
|
113
|
+
setSecureKeyAsync: async (account: string, value: string) =>
|
|
114
|
+
syncSet(account, value),
|
|
115
|
+
deleteSecureKeyAsync: async (account: string) => syncDelete(account),
|
|
78
116
|
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
79
117
|
getBackendType: () => "encrypted",
|
|
80
118
|
isDowngradedFromKeychain: () => false,
|
|
@@ -134,11 +172,26 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
|
134
172
|
|
|
135
173
|
import { handleMessage, type HandlerContext } from "../daemon/handlers.js";
|
|
136
174
|
import type {
|
|
175
|
+
ClientMessage,
|
|
137
176
|
ServerMessage,
|
|
138
177
|
TwitterIntegrationConfigRequest,
|
|
139
178
|
} from "../daemon/ipc-contract.js";
|
|
140
179
|
import { DebouncerMap } from "../util/debounce.js";
|
|
141
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Wrapper around handleMessage that flushes the microtask queue so async
|
|
183
|
+
* handlers complete before assertions run. handleMessage() returns void
|
|
184
|
+
* and swallows the promise, so we need a macrotask tick to settle.
|
|
185
|
+
*/
|
|
186
|
+
async function handleMessageAsync(
|
|
187
|
+
msg: ClientMessage,
|
|
188
|
+
socket: net.Socket,
|
|
189
|
+
ctx: HandlerContext,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
handleMessage(msg, socket, ctx);
|
|
192
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
193
|
+
}
|
|
194
|
+
|
|
142
195
|
function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
|
|
143
196
|
const sent: ServerMessage[] = [];
|
|
144
197
|
const ctx: HandlerContext = {
|
|
@@ -203,7 +256,7 @@ describe("Twitter integration config handler", () => {
|
|
|
203
256
|
});
|
|
204
257
|
|
|
205
258
|
test("get action returns correct status when configured and connected", () => {
|
|
206
|
-
rawConfigStore = {
|
|
259
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
207
260
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
208
261
|
"test-client-id";
|
|
209
262
|
secureKeyStore["credential:integration:twitter:access_token"] =
|
|
@@ -256,10 +309,12 @@ describe("Twitter integration config handler", () => {
|
|
|
256
309
|
expect(res.mode).toBe("managed");
|
|
257
310
|
|
|
258
311
|
expect(saveRawConfigCalls).toHaveLength(1);
|
|
259
|
-
expect(
|
|
312
|
+
expect(
|
|
313
|
+
getNestedValue(saveRawConfigCalls[0]!, "twitter.integrationMode"),
|
|
314
|
+
).toBe("managed");
|
|
260
315
|
});
|
|
261
316
|
|
|
262
|
-
test("set_local_client stores credentials in secure storage", () => {
|
|
317
|
+
test("set_local_client stores credentials in secure storage", async () => {
|
|
263
318
|
const msg: TwitterIntegrationConfigRequest = {
|
|
264
319
|
type: "twitter_integration_config",
|
|
265
320
|
action: "set_local_client",
|
|
@@ -268,7 +323,7 @@ describe("Twitter integration config handler", () => {
|
|
|
268
323
|
};
|
|
269
324
|
|
|
270
325
|
const { ctx, sent } = createTestContext();
|
|
271
|
-
|
|
326
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
272
327
|
|
|
273
328
|
expect(sent).toHaveLength(1);
|
|
274
329
|
const res = sent[0] as {
|
|
@@ -288,14 +343,14 @@ describe("Twitter integration config handler", () => {
|
|
|
288
343
|
).toBe("my-client-secret");
|
|
289
344
|
});
|
|
290
345
|
|
|
291
|
-
test("set_local_client without clientId returns error", () => {
|
|
346
|
+
test("set_local_client without clientId returns error", async () => {
|
|
292
347
|
const msg: TwitterIntegrationConfigRequest = {
|
|
293
348
|
type: "twitter_integration_config",
|
|
294
349
|
action: "set_local_client",
|
|
295
350
|
};
|
|
296
351
|
|
|
297
352
|
const { ctx, sent } = createTestContext();
|
|
298
|
-
|
|
353
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
299
354
|
|
|
300
355
|
expect(sent).toHaveLength(1);
|
|
301
356
|
const res = sent[0] as { type: string; success: boolean; error: string };
|
|
@@ -303,7 +358,7 @@ describe("Twitter integration config handler", () => {
|
|
|
303
358
|
expect(res.error).toContain("clientId is required");
|
|
304
359
|
});
|
|
305
360
|
|
|
306
|
-
test("clear_local_client removes credentials", () => {
|
|
361
|
+
test("clear_local_client removes credentials", async () => {
|
|
307
362
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
308
363
|
"my-client-id";
|
|
309
364
|
secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
|
|
@@ -315,7 +370,7 @@ describe("Twitter integration config handler", () => {
|
|
|
315
370
|
};
|
|
316
371
|
|
|
317
372
|
const { ctx, sent } = createTestContext();
|
|
318
|
-
|
|
373
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
319
374
|
|
|
320
375
|
expect(sent).toHaveLength(1);
|
|
321
376
|
const res = sent[0] as {
|
|
@@ -336,7 +391,7 @@ describe("Twitter integration config handler", () => {
|
|
|
336
391
|
).toBeUndefined();
|
|
337
392
|
});
|
|
338
393
|
|
|
339
|
-
test("clear_local_client also disconnects if connected", () => {
|
|
394
|
+
test("clear_local_client also disconnects if connected", async () => {
|
|
340
395
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
341
396
|
"my-client-id";
|
|
342
397
|
secureKeyStore["credential:integration:twitter:access_token"] =
|
|
@@ -355,7 +410,7 @@ describe("Twitter integration config handler", () => {
|
|
|
355
410
|
};
|
|
356
411
|
|
|
357
412
|
const { ctx, sent } = createTestContext();
|
|
358
|
-
|
|
413
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
359
414
|
|
|
360
415
|
expect(sent).toHaveLength(1);
|
|
361
416
|
const res = sent[0] as {
|
|
@@ -378,7 +433,7 @@ describe("Twitter integration config handler", () => {
|
|
|
378
433
|
});
|
|
379
434
|
});
|
|
380
435
|
|
|
381
|
-
test("disconnect removes tokens and metadata", () => {
|
|
436
|
+
test("disconnect removes tokens and metadata", async () => {
|
|
382
437
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
383
438
|
"my-client-id";
|
|
384
439
|
secureKeyStore["credential:integration:twitter:access_token"] =
|
|
@@ -397,7 +452,7 @@ describe("Twitter integration config handler", () => {
|
|
|
397
452
|
};
|
|
398
453
|
|
|
399
454
|
const { ctx, sent } = createTestContext();
|
|
400
|
-
|
|
455
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
401
456
|
|
|
402
457
|
expect(sent).toHaveLength(1);
|
|
403
458
|
const res = sent[0] as {
|
|
@@ -426,7 +481,7 @@ describe("Twitter integration config handler", () => {
|
|
|
426
481
|
});
|
|
427
482
|
});
|
|
428
483
|
|
|
429
|
-
test("set_local_client returns error when setSecureKey fails for client ID", () => {
|
|
484
|
+
test("set_local_client returns error when setSecureKey fails for client ID", async () => {
|
|
430
485
|
// Override setSecureKey to return false (storage unavailable, not throwing)
|
|
431
486
|
setSecureKeyOverride = () => false;
|
|
432
487
|
|
|
@@ -438,7 +493,7 @@ describe("Twitter integration config handler", () => {
|
|
|
438
493
|
};
|
|
439
494
|
|
|
440
495
|
const { ctx, sent } = createTestContext();
|
|
441
|
-
|
|
496
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
442
497
|
|
|
443
498
|
expect(sent).toHaveLength(1);
|
|
444
499
|
const res = sent[0] as {
|
|
@@ -453,7 +508,7 @@ describe("Twitter integration config handler", () => {
|
|
|
453
508
|
expect(res.error).toContain("Failed to store client ID");
|
|
454
509
|
});
|
|
455
510
|
|
|
456
|
-
test("set_local_client returns error when setSecureKey fails for client secret", () => {
|
|
511
|
+
test("set_local_client returns error when setSecureKey fails for client secret", async () => {
|
|
457
512
|
// Override setSecureKey to fail only for the secret
|
|
458
513
|
setSecureKeyOverride = (account: string, value: string) => {
|
|
459
514
|
if (account.includes("client_secret")) return false;
|
|
@@ -469,7 +524,7 @@ describe("Twitter integration config handler", () => {
|
|
|
469
524
|
};
|
|
470
525
|
|
|
471
526
|
const { ctx, sent } = createTestContext();
|
|
472
|
-
|
|
527
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
473
528
|
|
|
474
529
|
expect(sent).toHaveLength(1);
|
|
475
530
|
const res = sent[0] as {
|
|
@@ -484,7 +539,7 @@ describe("Twitter integration config handler", () => {
|
|
|
484
539
|
expect(res.error).toContain("Failed to store client secret");
|
|
485
540
|
});
|
|
486
541
|
|
|
487
|
-
test("set_local_client without secret clears stale secret", () => {
|
|
542
|
+
test("set_local_client without secret clears stale secret", async () => {
|
|
488
543
|
// Pre-populate an old client secret
|
|
489
544
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] = "old-id";
|
|
490
545
|
secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
|
|
@@ -497,7 +552,7 @@ describe("Twitter integration config handler", () => {
|
|
|
497
552
|
};
|
|
498
553
|
|
|
499
554
|
const { ctx, sent } = createTestContext();
|
|
500
|
-
|
|
555
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
501
556
|
|
|
502
557
|
expect(sent).toHaveLength(1);
|
|
503
558
|
const res = sent[0] as {
|
|
@@ -576,7 +631,7 @@ describe("Twitter integration config handler", () => {
|
|
|
576
631
|
expect((sent4[0] as { mode: string }).mode).toBe("local_byo");
|
|
577
632
|
});
|
|
578
633
|
|
|
579
|
-
test("set_local_client with only clientId (no secret)", () => {
|
|
634
|
+
test("set_local_client with only clientId (no secret)", async () => {
|
|
580
635
|
const msg: TwitterIntegrationConfigRequest = {
|
|
581
636
|
type: "twitter_integration_config",
|
|
582
637
|
action: "set_local_client",
|
|
@@ -584,7 +639,7 @@ describe("Twitter integration config handler", () => {
|
|
|
584
639
|
};
|
|
585
640
|
|
|
586
641
|
const { ctx, sent } = createTestContext();
|
|
587
|
-
|
|
642
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
588
643
|
|
|
589
644
|
expect(sent).toHaveLength(1);
|
|
590
645
|
const res = sent[0] as {
|
|
@@ -604,7 +659,7 @@ describe("Twitter integration config handler", () => {
|
|
|
604
659
|
).toBeUndefined();
|
|
605
660
|
});
|
|
606
661
|
|
|
607
|
-
test("set_local_client overwrites existing credentials", () => {
|
|
662
|
+
test("set_local_client overwrites existing credentials", async () => {
|
|
608
663
|
// Set initial credentials
|
|
609
664
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] = "old-id";
|
|
610
665
|
secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
|
|
@@ -618,7 +673,7 @@ describe("Twitter integration config handler", () => {
|
|
|
618
673
|
};
|
|
619
674
|
|
|
620
675
|
const { ctx, sent } = createTestContext();
|
|
621
|
-
|
|
676
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
622
677
|
|
|
623
678
|
expect(sent).toHaveLength(1);
|
|
624
679
|
const res = sent[0] as {
|
|
@@ -638,7 +693,7 @@ describe("Twitter integration config handler", () => {
|
|
|
638
693
|
).toBe("new-secret");
|
|
639
694
|
});
|
|
640
695
|
|
|
641
|
-
test("clear_local_client when no credentials exist (idempotent)", () => {
|
|
696
|
+
test("clear_local_client when no credentials exist (idempotent)", async () => {
|
|
642
697
|
// No credentials set at all
|
|
643
698
|
const msg: TwitterIntegrationConfigRequest = {
|
|
644
699
|
type: "twitter_integration_config",
|
|
@@ -646,7 +701,7 @@ describe("Twitter integration config handler", () => {
|
|
|
646
701
|
};
|
|
647
702
|
|
|
648
703
|
const { ctx, sent } = createTestContext();
|
|
649
|
-
|
|
704
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
650
705
|
|
|
651
706
|
expect(sent).toHaveLength(1);
|
|
652
707
|
const res = sent[0] as {
|
|
@@ -661,7 +716,7 @@ describe("Twitter integration config handler", () => {
|
|
|
661
716
|
expect(res.connected).toBe(false);
|
|
662
717
|
});
|
|
663
718
|
|
|
664
|
-
test("disconnect when not connected (idempotent) preserves client credentials", () => {
|
|
719
|
+
test("disconnect when not connected (idempotent) preserves client credentials", async () => {
|
|
665
720
|
// Only client credentials, no access token
|
|
666
721
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
667
722
|
"my-client-id";
|
|
@@ -674,7 +729,7 @@ describe("Twitter integration config handler", () => {
|
|
|
674
729
|
};
|
|
675
730
|
|
|
676
731
|
const { ctx, sent } = createTestContext();
|
|
677
|
-
|
|
732
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
678
733
|
|
|
679
734
|
expect(sent).toHaveLength(1);
|
|
680
735
|
const res = sent[0] as {
|
|
@@ -696,7 +751,7 @@ describe("Twitter integration config handler", () => {
|
|
|
696
751
|
).toBe("my-client-secret");
|
|
697
752
|
});
|
|
698
753
|
|
|
699
|
-
test("disconnect preserves client credentials when access token exists", () => {
|
|
754
|
+
test("disconnect preserves client credentials when access token exists", async () => {
|
|
700
755
|
// Set up both client credentials and tokens
|
|
701
756
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
702
757
|
"my-client-id";
|
|
@@ -718,7 +773,7 @@ describe("Twitter integration config handler", () => {
|
|
|
718
773
|
};
|
|
719
774
|
|
|
720
775
|
const { ctx, sent } = createTestContext();
|
|
721
|
-
|
|
776
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
722
777
|
|
|
723
778
|
expect(sent).toHaveLength(1);
|
|
724
779
|
const res = sent[0] as {
|
|
@@ -752,7 +807,7 @@ describe("Twitter integration config handler", () => {
|
|
|
752
807
|
});
|
|
753
808
|
});
|
|
754
809
|
|
|
755
|
-
test("clear_local_client cascades to remove tokens and metadata", () => {
|
|
810
|
+
test("clear_local_client cascades to remove tokens and metadata", async () => {
|
|
756
811
|
// Set up client credentials, tokens, and metadata
|
|
757
812
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
758
813
|
"my-client-id";
|
|
@@ -774,7 +829,7 @@ describe("Twitter integration config handler", () => {
|
|
|
774
829
|
};
|
|
775
830
|
|
|
776
831
|
const { ctx, sent } = createTestContext();
|
|
777
|
-
|
|
832
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
778
833
|
|
|
779
834
|
expect(sent).toHaveLength(1);
|
|
780
835
|
const res = sent[0] as {
|
|
@@ -860,7 +915,7 @@ describe("Twitter integration config handler", () => {
|
|
|
860
915
|
expect(res.connected).toBe(false);
|
|
861
916
|
});
|
|
862
917
|
|
|
863
|
-
test("error in secure storage throws and returns error response", () => {
|
|
918
|
+
test("error in secure storage throws and returns error response", async () => {
|
|
864
919
|
// Override setSecureKey to throw an error, simulating a storage failure
|
|
865
920
|
setSecureKeyOverride = () => {
|
|
866
921
|
throw new Error("Keychain access denied");
|
|
@@ -874,7 +929,7 @@ describe("Twitter integration config handler", () => {
|
|
|
874
929
|
};
|
|
875
930
|
|
|
876
931
|
const { ctx, sent } = createTestContext();
|
|
877
|
-
|
|
932
|
+
await handleMessageAsync(msg, {} as net.Socket, ctx);
|
|
878
933
|
|
|
879
934
|
expect(sent).toHaveLength(1);
|
|
880
935
|
const res = sent[0] as { type: string; success: boolean; error?: string };
|
|
@@ -984,7 +1039,9 @@ describe("Twitter integration config handler", () => {
|
|
|
984
1039
|
// Verify persistence via saveRawConfig
|
|
985
1040
|
expect(saveRawConfigCalls.length).toBeGreaterThan(0);
|
|
986
1041
|
const lastSaved = saveRawConfigCalls[saveRawConfigCalls.length - 1]!;
|
|
987
|
-
expect(lastSaved.
|
|
1042
|
+
expect(getNestedValue(lastSaved, "twitter.operationStrategy")).toBe(
|
|
1043
|
+
"oauth",
|
|
1044
|
+
);
|
|
988
1045
|
});
|
|
989
1046
|
|
|
990
1047
|
test("set_strategy with invalid value returns error", () => {
|
|
@@ -1022,7 +1079,7 @@ describe("Twitter integration config handler", () => {
|
|
|
1022
1079
|
|
|
1023
1080
|
test("get action includes strategy field with strategyConfigured=true when set", () => {
|
|
1024
1081
|
// Set a specific strategy first
|
|
1025
|
-
rawConfigStore = {
|
|
1082
|
+
rawConfigStore = { twitter: { operationStrategy: "browser" } };
|
|
1026
1083
|
|
|
1027
1084
|
const msg: TwitterIntegrationConfigRequest = {
|
|
1028
1085
|
type: "twitter_integration_config",
|
|
@@ -87,7 +87,7 @@ mock.module("../runtime/approval-message-composer.js", () => ({
|
|
|
87
87
|
}));
|
|
88
88
|
|
|
89
89
|
import { findContactChannel } from "../contacts/contact-store.js";
|
|
90
|
-
import {
|
|
90
|
+
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
91
91
|
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
92
92
|
import { createInvite, revokeInvite } from "../memory/invite-store.js";
|
|
93
93
|
import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
|
|
@@ -328,7 +328,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
328
328
|
|
|
329
329
|
test("existing active member sending normal message is unaffected", async () => {
|
|
330
330
|
// Pre-create an active member
|
|
331
|
-
|
|
331
|
+
upsertContactChannel({
|
|
332
332
|
sourceChannel: "telegram",
|
|
333
333
|
externalUserId: "user-active-member",
|
|
334
334
|
externalChatId: "chat-active",
|
|
@@ -377,7 +377,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
377
377
|
});
|
|
378
378
|
|
|
379
379
|
// Pre-create an active member that will click the invite link
|
|
380
|
-
|
|
380
|
+
upsertContactChannel({
|
|
381
381
|
sourceChannel: "telegram",
|
|
382
382
|
externalUserId: "user-already-active",
|
|
383
383
|
externalChatId: "chat-invite-test",
|
|
@@ -403,7 +403,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
403
403
|
maxUses: 5,
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
upsertContactChannel({
|
|
407
407
|
sourceChannel: "telegram",
|
|
408
408
|
externalUserId: "user-invite-123",
|
|
409
409
|
externalChatId: "chat-invite-test",
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
const secureKeyValues = new Map<string, string>();
|
|
4
|
+
let mockTwilioAccountSid: string | undefined;
|
|
4
5
|
|
|
5
6
|
mock.module("../security/secure-keys.js", () => ({
|
|
6
7
|
getSecureKey: (account: string) => secureKeyValues.get(account),
|
|
7
8
|
}));
|
|
8
9
|
|
|
10
|
+
mock.module("../config/loader.js", () => ({
|
|
11
|
+
loadConfig: () => ({
|
|
12
|
+
twilio: mockTwilioAccountSid
|
|
13
|
+
? { accountSid: mockTwilioAccountSid }
|
|
14
|
+
: undefined,
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
9
18
|
const { getIntegrationSummary, formatIntegrationSummary, hasCapability } =
|
|
10
19
|
await import("../schedule/integration-status.js");
|
|
11
20
|
|
|
12
21
|
describe("integration-status", () => {
|
|
13
22
|
beforeEach(() => {
|
|
14
23
|
secureKeyValues.clear();
|
|
24
|
+
mockTwilioAccountSid = undefined;
|
|
15
25
|
});
|
|
16
26
|
|
|
17
27
|
describe("getIntegrationSummary", () => {
|
|
@@ -30,7 +40,7 @@ describe("integration-status", () => {
|
|
|
30
40
|
secureKeyValues.set("credential:integration:gmail:access_token", "tok");
|
|
31
41
|
secureKeyValues.set("credential:integration:twitter:access_token", "tok");
|
|
32
42
|
secureKeyValues.set("credential:integration:slack:access_token", "tok");
|
|
33
|
-
|
|
43
|
+
mockTwilioAccountSid = "sid";
|
|
34
44
|
secureKeyValues.set("credential:twilio:auth_token", "auth");
|
|
35
45
|
secureKeyValues.set("credential:telegram:bot_token", "tok");
|
|
36
46
|
secureKeyValues.set("credential:telegram:webhook_secret", "secret");
|
|
@@ -42,7 +52,7 @@ describe("integration-status", () => {
|
|
|
42
52
|
});
|
|
43
53
|
|
|
44
54
|
test("returns mixed status", () => {
|
|
45
|
-
|
|
55
|
+
mockTwilioAccountSid = "sid";
|
|
46
56
|
secureKeyValues.set("credential:twilio:auth_token", "auth");
|
|
47
57
|
secureKeyValues.set("credential:telegram:bot_token", "tok");
|
|
48
58
|
secureKeyValues.set("credential:telegram:webhook_secret", "secret");
|
|
@@ -67,7 +77,7 @@ describe("integration-status", () => {
|
|
|
67
77
|
});
|
|
68
78
|
|
|
69
79
|
test("SMS disconnected when only account_sid is set (missing auth_token)", () => {
|
|
70
|
-
|
|
80
|
+
mockTwilioAccountSid = "sid";
|
|
71
81
|
|
|
72
82
|
const summary = getIntegrationSummary();
|
|
73
83
|
const sms = summary.find((s: { name: string }) => s.name === "SMS");
|
|
@@ -87,7 +97,7 @@ describe("integration-status", () => {
|
|
|
87
97
|
|
|
88
98
|
describe("formatIntegrationSummary", () => {
|
|
89
99
|
test("shows checkmarks and crosses", () => {
|
|
90
|
-
|
|
100
|
+
mockTwilioAccountSid = "sid";
|
|
91
101
|
secureKeyValues.set("credential:twilio:auth_token", "auth");
|
|
92
102
|
secureKeyValues.set("credential:telegram:bot_token", "tok");
|
|
93
103
|
secureKeyValues.set("credential:telegram:webhook_secret", "secret");
|
|
@@ -109,7 +119,7 @@ describe("integration-status", () => {
|
|
|
109
119
|
secureKeyValues.set("credential:integration:gmail:access_token", "tok");
|
|
110
120
|
secureKeyValues.set("credential:integration:twitter:access_token", "tok");
|
|
111
121
|
secureKeyValues.set("credential:integration:slack:access_token", "tok");
|
|
112
|
-
|
|
122
|
+
mockTwilioAccountSid = "sid";
|
|
113
123
|
secureKeyValues.set("credential:twilio:auth_token", "auth");
|
|
114
124
|
secureKeyValues.set("credential:telegram:bot_token", "tok");
|
|
115
125
|
secureKeyValues.set("credential:telegram:webhook_secret", "secret");
|
|
@@ -24,7 +24,8 @@ mock.module("../util/logger.js", () => ({
|
|
|
24
24
|
}),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { findContactChannel } from "../contacts/contact-store.js";
|
|
28
|
+
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
28
29
|
import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
|
|
29
30
|
import {
|
|
30
31
|
createInvite,
|
|
@@ -33,7 +34,9 @@ import {
|
|
|
33
34
|
import {
|
|
34
35
|
type InviteRedemptionOutcome,
|
|
35
36
|
redeemInvite,
|
|
37
|
+
redeemInviteByCode,
|
|
36
38
|
} from "../runtime/invite-redemption-service.js";
|
|
39
|
+
import { hashVoiceCode } from "../util/voice-code.js";
|
|
37
40
|
|
|
38
41
|
initializeDb();
|
|
39
42
|
|
|
@@ -76,6 +79,58 @@ describe("invite-redemption-service", () => {
|
|
|
76
79
|
});
|
|
77
80
|
});
|
|
78
81
|
|
|
82
|
+
test("marks channel as verified via invite on redemption", () => {
|
|
83
|
+
const { rawToken } = createInvite({
|
|
84
|
+
sourceChannel: "telegram",
|
|
85
|
+
maxUses: 1,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const outcome = redeemInvite({
|
|
89
|
+
rawToken,
|
|
90
|
+
sourceChannel: "telegram",
|
|
91
|
+
externalUserId: "user-1",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(outcome.ok).toBe(true);
|
|
95
|
+
|
|
96
|
+
const result = findContactChannel({
|
|
97
|
+
channelType: "telegram",
|
|
98
|
+
externalUserId: "user-1",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result).not.toBeNull();
|
|
102
|
+
expect(result!.channel.verifiedAt).toBeGreaterThan(0);
|
|
103
|
+
expect(result!.channel.verifiedVia).toBe("invite");
|
|
104
|
+
expect(result!.channel.status).toBe("active");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("marks channel as verified via invite on 6-digit code redemption", () => {
|
|
108
|
+
const inviteCode = "123456";
|
|
109
|
+
createInvite({
|
|
110
|
+
sourceChannel: "telegram",
|
|
111
|
+
maxUses: 1,
|
|
112
|
+
inviteCodeHash: hashVoiceCode(inviteCode),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const outcome = redeemInviteByCode({
|
|
116
|
+
code: inviteCode,
|
|
117
|
+
sourceChannel: "telegram",
|
|
118
|
+
externalUserId: "code-user-1",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(outcome.ok).toBe(true);
|
|
122
|
+
|
|
123
|
+
const result = findContactChannel({
|
|
124
|
+
channelType: "telegram",
|
|
125
|
+
externalUserId: "code-user-1",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result).not.toBeNull();
|
|
129
|
+
expect(result!.channel.verifiedAt).toBeGreaterThan(0);
|
|
130
|
+
expect(result!.channel.verifiedVia).toBe("invite");
|
|
131
|
+
expect(result!.channel.status).toBe("active");
|
|
132
|
+
});
|
|
133
|
+
|
|
79
134
|
test("returns invalid_token for a bogus token", () => {
|
|
80
135
|
const outcome = redeemInvite({
|
|
81
136
|
rawToken: "totally-bogus-token",
|
|
@@ -179,7 +234,7 @@ describe("invite-redemption-service", () => {
|
|
|
179
234
|
});
|
|
180
235
|
|
|
181
236
|
// Pre-create an active member
|
|
182
|
-
|
|
237
|
+
upsertContactChannel({
|
|
183
238
|
sourceChannel: "telegram",
|
|
184
239
|
externalUserId: "existing-user",
|
|
185
240
|
status: "active",
|
|
@@ -209,7 +264,7 @@ describe("invite-redemption-service", () => {
|
|
|
209
264
|
});
|
|
210
265
|
|
|
211
266
|
// Pre-create a blocked member — simulates a guardian-initiated block
|
|
212
|
-
|
|
267
|
+
upsertContactChannel({
|
|
213
268
|
sourceChannel: "telegram",
|
|
214
269
|
externalUserId: "blocked-user",
|
|
215
270
|
status: "blocked",
|
|
@@ -231,7 +286,7 @@ describe("invite-redemption-service", () => {
|
|
|
231
286
|
});
|
|
232
287
|
|
|
233
288
|
// Pre-create a revoked member
|
|
234
|
-
const member =
|
|
289
|
+
const member = upsertContactChannel({
|
|
235
290
|
sourceChannel: "telegram",
|
|
236
291
|
externalUserId: "revoked-user",
|
|
237
292
|
status: "revoked",
|
|
@@ -282,7 +337,7 @@ describe("invite-redemption-service", () => {
|
|
|
282
337
|
|
|
283
338
|
test("returns invalid_token for an active member with a bogus token (no membership probing)", () => {
|
|
284
339
|
// Pre-create an active member
|
|
285
|
-
|
|
340
|
+
upsertContactChannel({
|
|
286
341
|
sourceChannel: "telegram",
|
|
287
342
|
externalUserId: "probed-user",
|
|
288
343
|
status: "active",
|
|
@@ -307,7 +362,7 @@ describe("invite-redemption-service", () => {
|
|
|
307
362
|
});
|
|
308
363
|
|
|
309
364
|
// Pre-create an active member
|
|
310
|
-
|
|
365
|
+
upsertContactChannel({
|
|
311
366
|
sourceChannel: "telegram",
|
|
312
367
|
externalUserId: "expired-token-user",
|
|
313
368
|
status: "active",
|
|
@@ -331,7 +386,7 @@ describe("invite-redemption-service", () => {
|
|
|
331
386
|
});
|
|
332
387
|
|
|
333
388
|
// Pre-create an active member on telegram
|
|
334
|
-
|
|
389
|
+
upsertContactChannel({
|
|
335
390
|
sourceChannel: "telegram",
|
|
336
391
|
externalUserId: "cross-channel-user",
|
|
337
392
|
status: "active",
|
|
@@ -398,10 +398,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
398
398
|
type: "share_app_cloud",
|
|
399
399
|
appId: "app-001",
|
|
400
400
|
},
|
|
401
|
-
share_to_slack: {
|
|
402
|
-
type: "share_to_slack",
|
|
403
|
-
appId: "app-001",
|
|
404
|
-
},
|
|
405
401
|
slack_webhook_config: {
|
|
406
402
|
type: "slack_webhook_config",
|
|
407
403
|
action: "get",
|
|
@@ -1450,10 +1446,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1450
1446
|
shareToken: "abc123def456",
|
|
1451
1447
|
shareUrl: "http://localhost:7821/v1/apps/shared/abc123def456",
|
|
1452
1448
|
},
|
|
1453
|
-
share_to_slack_response: {
|
|
1454
|
-
type: "share_to_slack_response",
|
|
1455
|
-
success: true,
|
|
1456
|
-
},
|
|
1457
1449
|
slack_webhook_config_response: {
|
|
1458
1450
|
type: "slack_webhook_config_response",
|
|
1459
1451
|
webhookUrl: "https://hooks.slack.com/services/T00/B00/xxx",
|