@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,17 +10,30 @@ const testDir = mkdtempSync(join(tmpdir(), "handlers-twitter-auth-test-"));
|
|
|
10
10
|
let rawConfigStore: Record<string, unknown> = {};
|
|
11
11
|
let mockIngressPublicBaseUrl: string | undefined = "https://test.example.com";
|
|
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
|
+
|
|
13
25
|
mock.module("../config/loader.js", () => ({
|
|
14
26
|
getConfig: () => ({
|
|
15
27
|
ui: {},
|
|
16
28
|
}),
|
|
17
29
|
loadConfig: () => ({ ingress: { publicBaseUrl: mockIngressPublicBaseUrl } }),
|
|
18
|
-
loadRawConfig: () => (
|
|
30
|
+
loadRawConfig: () => structuredClone(rawConfigStore),
|
|
19
31
|
saveRawConfig: (cfg: Record<string, unknown>) => {
|
|
20
|
-
rawConfigStore =
|
|
32
|
+
rawConfigStore = structuredClone(cfg);
|
|
21
33
|
},
|
|
22
34
|
saveConfig: () => {},
|
|
23
35
|
invalidateConfigCache: () => {},
|
|
36
|
+
getNestedValue,
|
|
24
37
|
}));
|
|
25
38
|
|
|
26
39
|
mock.module("../inbound/public-ingress-urls.js", () => ({
|
|
@@ -207,7 +220,7 @@ describe("Twitter auth handler", () => {
|
|
|
207
220
|
|
|
208
221
|
describe("twitter_auth_start", () => {
|
|
209
222
|
test("fails if mode is not local_byo", async () => {
|
|
210
|
-
rawConfigStore = {
|
|
223
|
+
rawConfigStore = { twitter: { integrationMode: "managed" } };
|
|
211
224
|
|
|
212
225
|
const msg: TwitterAuthStartRequest = { type: "twitter_auth_start" };
|
|
213
226
|
const { ctx, sent } = createTestContext();
|
|
@@ -228,7 +241,7 @@ describe("Twitter auth handler", () => {
|
|
|
228
241
|
});
|
|
229
242
|
|
|
230
243
|
test("fails if no client credentials configured", async () => {
|
|
231
|
-
rawConfigStore = {
|
|
244
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
232
245
|
// No client ID in secure storage
|
|
233
246
|
|
|
234
247
|
const msg: TwitterAuthStartRequest = { type: "twitter_auth_start" };
|
|
@@ -249,7 +262,7 @@ describe("Twitter auth handler", () => {
|
|
|
249
262
|
});
|
|
250
263
|
|
|
251
264
|
test("succeeds with valid config (mock orchestrator)", async () => {
|
|
252
|
-
rawConfigStore = {
|
|
265
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
253
266
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
254
267
|
"test-client-id";
|
|
255
268
|
secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
|
|
@@ -283,7 +296,7 @@ describe("Twitter auth handler", () => {
|
|
|
283
296
|
});
|
|
284
297
|
|
|
285
298
|
test("delegates to orchestrateOAuthConnect with correct options", async () => {
|
|
286
|
-
rawConfigStore = {
|
|
299
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
287
300
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
288
301
|
"test-client-id";
|
|
289
302
|
secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
|
|
@@ -312,7 +325,7 @@ describe("Twitter auth handler", () => {
|
|
|
312
325
|
});
|
|
313
326
|
|
|
314
327
|
test("fails fast with actionable error when no ingress URL is configured", async () => {
|
|
315
|
-
rawConfigStore = {
|
|
328
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
316
329
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
317
330
|
"test-client-id";
|
|
318
331
|
mockIngressPublicBaseUrl = undefined;
|
|
@@ -349,7 +362,7 @@ describe("Twitter auth handler", () => {
|
|
|
349
362
|
});
|
|
350
363
|
|
|
351
364
|
test("maps orchestrator error result to twitter_auth_result", async () => {
|
|
352
|
-
rawConfigStore = {
|
|
365
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
353
366
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
354
367
|
"test-client-id";
|
|
355
368
|
|
|
@@ -379,7 +392,7 @@ describe("Twitter auth handler", () => {
|
|
|
379
392
|
|
|
380
393
|
describe("auth hardening", () => {
|
|
381
394
|
test("OAuth cancel path returns sanitized failure", async () => {
|
|
382
|
-
rawConfigStore = {
|
|
395
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
383
396
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
384
397
|
"test-client-id";
|
|
385
398
|
|
|
@@ -404,7 +417,7 @@ describe("Twitter auth handler", () => {
|
|
|
404
417
|
});
|
|
405
418
|
|
|
406
419
|
test("OAuth timeout path returns sanitized failure", async () => {
|
|
407
|
-
rawConfigStore = {
|
|
420
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
408
421
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
409
422
|
"test-client-id";
|
|
410
423
|
|
|
@@ -431,7 +444,7 @@ describe("Twitter auth handler", () => {
|
|
|
431
444
|
});
|
|
432
445
|
|
|
433
446
|
test("error payload never includes secrets or raw provider bodies", async () => {
|
|
434
|
-
rawConfigStore = {
|
|
447
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
435
448
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
436
449
|
"test-client-id";
|
|
437
450
|
|
|
@@ -466,7 +479,7 @@ describe("Twitter auth handler", () => {
|
|
|
466
479
|
});
|
|
467
480
|
|
|
468
481
|
test("succeeds even when identity verification returns no accountInfo", async () => {
|
|
469
|
-
rawConfigStore = {
|
|
482
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
470
483
|
secureKeyStore["credential:integration:twitter:oauth_client_id"] =
|
|
471
484
|
"test-client-id";
|
|
472
485
|
|
|
@@ -499,7 +512,7 @@ describe("Twitter auth handler", () => {
|
|
|
499
512
|
|
|
500
513
|
describe("twitter_auth_status", () => {
|
|
501
514
|
test("returns disconnected when no token exists", () => {
|
|
502
|
-
rawConfigStore = {
|
|
515
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
503
516
|
|
|
504
517
|
const msg: TwitterAuthStatusRequest = { type: "twitter_auth_status" };
|
|
505
518
|
const { ctx, sent } = createTestContext();
|
|
@@ -519,7 +532,7 @@ describe("Twitter auth handler", () => {
|
|
|
519
532
|
});
|
|
520
533
|
|
|
521
534
|
test("returns connected with account info when token exists", () => {
|
|
522
|
-
rawConfigStore = {
|
|
535
|
+
rawConfigStore = { twitter: { integrationMode: "local_byo" } };
|
|
523
536
|
secureKeyStore["credential:integration:twitter:access_token"] =
|
|
524
537
|
"test-access-token";
|
|
525
538
|
credentialMetadataStore.push({
|
|
@@ -19,7 +19,7 @@ import { SessionExpiredError } from "../twitter/client.js";
|
|
|
19
19
|
|
|
20
20
|
const SESSION_EXPIRED_MSG =
|
|
21
21
|
"Your Twitter session has expired. Please sign in to Twitter in Chrome — " +
|
|
22
|
-
"run `
|
|
22
|
+
"run `assistant twitter refresh` to capture your session automatically.";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Replicates the error-to-payload logic from `run()` in twitter.ts.
|
|
@@ -2,8 +2,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
// --- Mocks (must be declared before importing the module under test) ---
|
|
4
4
|
|
|
5
|
-
let mockStrategy: string | undefined = undefined;
|
|
6
|
-
let mockOauthAvailable = false;
|
|
7
5
|
let mockOauthPostResult: {
|
|
8
6
|
tweetId: string;
|
|
9
7
|
text: string;
|
|
@@ -17,33 +15,13 @@ let mockBrowserPostResult: {
|
|
|
17
15
|
} | null = null;
|
|
18
16
|
let mockBrowserPostError: Error | null = null;
|
|
19
17
|
|
|
20
|
-
// Mock the config loader to return a controllable strategy
|
|
21
|
-
mock.module("../config/loader.js", () => ({
|
|
22
|
-
loadRawConfig: () => {
|
|
23
|
-
if (mockStrategy !== undefined) {
|
|
24
|
-
return { twitterOperationStrategy: mockStrategy };
|
|
25
|
-
}
|
|
26
|
-
return {};
|
|
27
|
-
},
|
|
28
|
-
loadConfig: () => ({}),
|
|
29
|
-
saveConfig: () => {},
|
|
30
|
-
saveRawConfig: () => {},
|
|
31
|
-
getConfig: () => ({
|
|
32
|
-
ui: {},
|
|
33
|
-
}),
|
|
34
|
-
invalidateConfigCache: () => {},
|
|
35
|
-
getNestedValue: () => undefined,
|
|
36
|
-
setNestedValue: () => {},
|
|
37
|
-
API_KEY_PROVIDERS: [],
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
18
|
// Mock the OAuth client
|
|
41
19
|
mock.module("../twitter/oauth-client.js", () => ({
|
|
42
|
-
oauthIsAvailable: () =>
|
|
20
|
+
oauthIsAvailable: (token?: string) => token != null && token.length > 0,
|
|
43
21
|
oauthSupportsOperation: (op: string) => op === "post" || op === "reply",
|
|
44
22
|
oauthPostTweet: async (
|
|
45
23
|
_text: string,
|
|
46
|
-
_opts
|
|
24
|
+
_opts: { inReplyToTweetId?: string; oauthToken: string },
|
|
47
25
|
) => {
|
|
48
26
|
if (mockOauthPostError) throw mockOauthPostError;
|
|
49
27
|
if (mockOauthPostResult) return mockOauthPostResult;
|
|
@@ -100,8 +78,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
100
78
|
import { routedPostTweet } from "../twitter/router.js";
|
|
101
79
|
|
|
102
80
|
beforeEach(() => {
|
|
103
|
-
mockStrategy = undefined;
|
|
104
|
-
mockOauthAvailable = false;
|
|
105
81
|
mockOauthPostResult = null;
|
|
106
82
|
mockOauthPostError = null;
|
|
107
83
|
mockBrowserPostResult = null;
|
|
@@ -111,14 +87,16 @@ beforeEach(() => {
|
|
|
111
87
|
describe("Twitter strategy router", () => {
|
|
112
88
|
describe("auto strategy", () => {
|
|
113
89
|
test("uses OAuth when available and supported", async () => {
|
|
114
|
-
mockOauthAvailable = true;
|
|
115
90
|
mockOauthPostResult = {
|
|
116
91
|
tweetId: "111",
|
|
117
92
|
text: "hello",
|
|
118
93
|
url: "https://x.com/u/status/111",
|
|
119
94
|
};
|
|
120
95
|
|
|
121
|
-
const { result, pathUsed } = await routedPostTweet("hello"
|
|
96
|
+
const { result, pathUsed } = await routedPostTweet("hello", {
|
|
97
|
+
strategy: "auto",
|
|
98
|
+
oauthToken: "test-token",
|
|
99
|
+
});
|
|
122
100
|
|
|
123
101
|
expect(pathUsed).toBe("oauth");
|
|
124
102
|
expect(result.tweetId).toBe("111");
|
|
@@ -127,21 +105,21 @@ describe("Twitter strategy router", () => {
|
|
|
127
105
|
});
|
|
128
106
|
|
|
129
107
|
test("falls back to browser when OAuth is unavailable", async () => {
|
|
130
|
-
mockOauthAvailable = false;
|
|
131
108
|
mockBrowserPostResult = {
|
|
132
109
|
tweetId: "222",
|
|
133
110
|
text: "hello",
|
|
134
111
|
url: "https://x.com/u/status/222",
|
|
135
112
|
};
|
|
136
113
|
|
|
137
|
-
const { result, pathUsed } = await routedPostTweet("hello"
|
|
114
|
+
const { result, pathUsed } = await routedPostTweet("hello", {
|
|
115
|
+
strategy: "auto",
|
|
116
|
+
});
|
|
138
117
|
|
|
139
118
|
expect(pathUsed).toBe("browser");
|
|
140
119
|
expect(result.tweetId).toBe("222");
|
|
141
120
|
});
|
|
142
121
|
|
|
143
122
|
test("falls back to browser when OAuth fails", async () => {
|
|
144
|
-
mockOauthAvailable = true;
|
|
145
123
|
mockOauthPostError = new Error("OAuth token expired");
|
|
146
124
|
mockBrowserPostResult = {
|
|
147
125
|
tweetId: "333",
|
|
@@ -149,31 +127,38 @@ describe("Twitter strategy router", () => {
|
|
|
149
127
|
url: "https://x.com/u/status/333",
|
|
150
128
|
};
|
|
151
129
|
|
|
152
|
-
const { result, pathUsed } = await routedPostTweet("hello"
|
|
130
|
+
const { result, pathUsed } = await routedPostTweet("hello", {
|
|
131
|
+
strategy: "auto",
|
|
132
|
+
oauthToken: "test-token",
|
|
133
|
+
});
|
|
153
134
|
|
|
154
135
|
expect(pathUsed).toBe("browser");
|
|
155
136
|
expect(result.tweetId).toBe("333");
|
|
156
137
|
});
|
|
157
138
|
|
|
158
139
|
test("constructs URL from tweetId when OAuth result has no url", async () => {
|
|
159
|
-
mockOauthAvailable = true;
|
|
160
140
|
mockOauthPostResult = { tweetId: "444", text: "no url" };
|
|
161
141
|
|
|
162
|
-
const { result, pathUsed } = await routedPostTweet("no url"
|
|
142
|
+
const { result, pathUsed } = await routedPostTweet("no url", {
|
|
143
|
+
strategy: "auto",
|
|
144
|
+
oauthToken: "test-token",
|
|
145
|
+
});
|
|
163
146
|
|
|
164
147
|
expect(pathUsed).toBe("oauth");
|
|
165
148
|
expect(result.url).toBe("https://x.com/i/status/444");
|
|
166
149
|
});
|
|
167
150
|
|
|
168
151
|
test("throws combined error when both OAuth and browser fail with SessionExpiredError", async () => {
|
|
169
|
-
mockOauthAvailable = true;
|
|
170
152
|
mockOauthPostError = new Error("OAuth failed");
|
|
171
153
|
mockBrowserPostError = new MockSessionExpiredError(
|
|
172
154
|
"Browser session expired",
|
|
173
155
|
);
|
|
174
156
|
|
|
175
157
|
try {
|
|
176
|
-
await routedPostTweet("will fail"
|
|
158
|
+
await routedPostTweet("will fail", {
|
|
159
|
+
strategy: "auto",
|
|
160
|
+
oauthToken: "test-token",
|
|
161
|
+
});
|
|
177
162
|
expect(true).toBe(false); // should not reach
|
|
178
163
|
} catch (err) {
|
|
179
164
|
const e = err as Error & { pathUsed: string; oauthError?: string };
|
|
@@ -187,11 +172,8 @@ describe("Twitter strategy router", () => {
|
|
|
187
172
|
|
|
188
173
|
describe("explicit oauth strategy", () => {
|
|
189
174
|
test("fails with helpful error when OAuth is not configured", async () => {
|
|
190
|
-
mockStrategy = "oauth";
|
|
191
|
-
mockOauthAvailable = false;
|
|
192
|
-
|
|
193
175
|
try {
|
|
194
|
-
await routedPostTweet("hello");
|
|
176
|
+
await routedPostTweet("hello", { strategy: "oauth" });
|
|
195
177
|
expect(true).toBe(false); // should not reach
|
|
196
178
|
} catch (err) {
|
|
197
179
|
const e = err as Error & {
|
|
@@ -199,18 +181,21 @@ describe("Twitter strategy router", () => {
|
|
|
199
181
|
suggestAlternative: string;
|
|
200
182
|
};
|
|
201
183
|
expect(e.message).toContain("OAuth is not configured");
|
|
202
|
-
expect(e.message).toContain(
|
|
184
|
+
expect(e.message).toContain(
|
|
185
|
+
"assistant config set twitter.operationStrategy browser",
|
|
186
|
+
);
|
|
203
187
|
expect(e.pathUsed).toBe("oauth");
|
|
204
188
|
expect(e.suggestAlternative).toBe("browser");
|
|
205
189
|
}
|
|
206
190
|
});
|
|
207
191
|
|
|
208
192
|
test("uses OAuth when available", async () => {
|
|
209
|
-
mockStrategy = "oauth";
|
|
210
|
-
mockOauthAvailable = true;
|
|
211
193
|
mockOauthPostResult = { tweetId: "555", text: "oauth post" };
|
|
212
194
|
|
|
213
|
-
const { result, pathUsed } = await routedPostTweet("oauth post"
|
|
195
|
+
const { result, pathUsed } = await routedPostTweet("oauth post", {
|
|
196
|
+
strategy: "oauth",
|
|
197
|
+
oauthToken: "test-token",
|
|
198
|
+
});
|
|
214
199
|
|
|
215
200
|
expect(pathUsed).toBe("oauth");
|
|
216
201
|
expect(result.tweetId).toBe("555");
|
|
@@ -219,26 +204,26 @@ describe("Twitter strategy router", () => {
|
|
|
219
204
|
|
|
220
205
|
describe("explicit browser strategy", () => {
|
|
221
206
|
test("uses browser directly, ignoring OAuth availability", async () => {
|
|
222
|
-
mockStrategy = "browser";
|
|
223
|
-
mockOauthAvailable = true; // available but should be ignored
|
|
224
207
|
mockBrowserPostResult = {
|
|
225
208
|
tweetId: "666",
|
|
226
209
|
text: "browser post",
|
|
227
210
|
url: "https://x.com/u/status/666",
|
|
228
211
|
};
|
|
229
212
|
|
|
230
|
-
const { result, pathUsed } = await routedPostTweet("browser post"
|
|
213
|
+
const { result, pathUsed } = await routedPostTweet("browser post", {
|
|
214
|
+
strategy: "browser",
|
|
215
|
+
oauthToken: "test-token", // available but should be ignored
|
|
216
|
+
});
|
|
231
217
|
|
|
232
218
|
expect(pathUsed).toBe("browser");
|
|
233
219
|
expect(result.tweetId).toBe("666");
|
|
234
220
|
});
|
|
235
221
|
|
|
236
222
|
test("preserves SessionExpiredError type with router metadata", async () => {
|
|
237
|
-
mockStrategy = "browser";
|
|
238
223
|
mockBrowserPostError = new MockSessionExpiredError("Session expired");
|
|
239
224
|
|
|
240
225
|
try {
|
|
241
|
-
await routedPostTweet("will fail");
|
|
226
|
+
await routedPostTweet("will fail", { strategy: "browser" });
|
|
242
227
|
expect(true).toBe(false); // should not reach
|
|
243
228
|
} catch (err) {
|
|
244
229
|
const e = err as Error & {
|
|
@@ -253,11 +238,10 @@ describe("Twitter strategy router", () => {
|
|
|
253
238
|
});
|
|
254
239
|
|
|
255
240
|
test("re-throws non-session errors without wrapping", async () => {
|
|
256
|
-
mockStrategy = "browser";
|
|
257
241
|
mockBrowserPostError = new Error("Network failure");
|
|
258
242
|
|
|
259
243
|
try {
|
|
260
|
-
await routedPostTweet("will fail");
|
|
244
|
+
await routedPostTweet("will fail", { strategy: "browser" });
|
|
261
245
|
expect(true).toBe(false); // should not reach
|
|
262
246
|
} catch (err) {
|
|
263
247
|
expect((err as Error).message).toBe("Network failure");
|
|
@@ -267,7 +251,6 @@ describe("Twitter strategy router", () => {
|
|
|
267
251
|
|
|
268
252
|
describe("reply routing", () => {
|
|
269
253
|
test("auto strategy routes reply through OAuth when available", async () => {
|
|
270
|
-
mockOauthAvailable = true;
|
|
271
254
|
mockOauthPostResult = {
|
|
272
255
|
tweetId: "777",
|
|
273
256
|
text: "reply text",
|
|
@@ -276,6 +259,8 @@ describe("Twitter strategy router", () => {
|
|
|
276
259
|
|
|
277
260
|
const { result, pathUsed } = await routedPostTweet("reply text", {
|
|
278
261
|
inReplyToTweetId: "100",
|
|
262
|
+
strategy: "auto",
|
|
263
|
+
oauthToken: "test-token",
|
|
279
264
|
});
|
|
280
265
|
|
|
281
266
|
expect(pathUsed).toBe("oauth");
|
|
@@ -283,7 +268,6 @@ describe("Twitter strategy router", () => {
|
|
|
283
268
|
});
|
|
284
269
|
|
|
285
270
|
test("browser strategy routes reply through browser", async () => {
|
|
286
|
-
mockStrategy = "browser";
|
|
287
271
|
mockBrowserPostResult = {
|
|
288
272
|
tweetId: "888",
|
|
289
273
|
text: "reply text",
|
|
@@ -292,6 +276,7 @@ describe("Twitter strategy router", () => {
|
|
|
292
276
|
|
|
293
277
|
const { result, pathUsed } = await routedPostTweet("reply text", {
|
|
294
278
|
inReplyToTweetId: "200",
|
|
279
|
+
strategy: "browser",
|
|
295
280
|
});
|
|
296
281
|
|
|
297
282
|
expect(pathUsed).toBe("browser");
|
|
@@ -2,39 +2,6 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
// --- Mocks (must be declared before importing the module under test) ---
|
|
4
4
|
|
|
5
|
-
let secureKeyStore: Record<string, string> = {};
|
|
6
|
-
|
|
7
|
-
mock.module("../security/secure-keys.js", () => ({
|
|
8
|
-
getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
|
|
9
|
-
setSecureKey: (account: string, value: string) => {
|
|
10
|
-
secureKeyStore[account] = value;
|
|
11
|
-
return true;
|
|
12
|
-
},
|
|
13
|
-
deleteSecureKey: () => "deleted",
|
|
14
|
-
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
15
|
-
getBackendType: () => "encrypted",
|
|
16
|
-
isDowngradedFromKeychain: () => false,
|
|
17
|
-
_resetBackend: () => {},
|
|
18
|
-
_setBackend: () => {},
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// withValidToken: call the callback directly with a fake token.
|
|
22
|
-
mock.module("../security/token-manager.js", () => ({
|
|
23
|
-
withValidToken: async (
|
|
24
|
-
_service: string,
|
|
25
|
-
cb: (token: string) => Promise<unknown>,
|
|
26
|
-
) => cb("fake-oauth-token"),
|
|
27
|
-
TokenExpiredError: class TokenExpiredError extends Error {
|
|
28
|
-
constructor(
|
|
29
|
-
public readonly service: string,
|
|
30
|
-
message?: string,
|
|
31
|
-
) {
|
|
32
|
-
super(message ?? `Token expired for "${service}".`);
|
|
33
|
-
this.name = "TokenExpiredError";
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
5
|
mock.module("../util/logger.js", () => ({
|
|
39
6
|
getLogger: () => ({
|
|
40
7
|
info: () => {},
|
|
@@ -65,7 +32,6 @@ const originalFetch = globalThis.fetch;
|
|
|
65
32
|
let _fetchMock: ReturnType<typeof mock> | null = null;
|
|
66
33
|
|
|
67
34
|
beforeEach(() => {
|
|
68
|
-
secureKeyStore = {};
|
|
69
35
|
_fetchMock = null;
|
|
70
36
|
});
|
|
71
37
|
|
|
@@ -101,7 +67,9 @@ describe("Twitter OAuth client", () => {
|
|
|
101
67
|
json: { data: { id: "12345", text: "Hello world" } },
|
|
102
68
|
});
|
|
103
69
|
|
|
104
|
-
const result = await oauthPostTweet("Hello world"
|
|
70
|
+
const result = await oauthPostTweet("Hello world", {
|
|
71
|
+
oauthToken: "fake-oauth-token",
|
|
72
|
+
});
|
|
105
73
|
|
|
106
74
|
expect(result.tweetId).toBe("12345");
|
|
107
75
|
expect(result.text).toBe("Hello world");
|
|
@@ -132,6 +100,7 @@ describe("Twitter OAuth client", () => {
|
|
|
132
100
|
|
|
133
101
|
const result = await oauthPostTweet("My reply", {
|
|
134
102
|
inReplyToTweetId: "11111",
|
|
103
|
+
oauthToken: "fake-oauth-token",
|
|
135
104
|
});
|
|
136
105
|
|
|
137
106
|
expect(result.tweetId).toBe("67890");
|
|
@@ -150,12 +119,12 @@ describe("Twitter OAuth client", () => {
|
|
|
150
119
|
text: "Rate limit exceeded",
|
|
151
120
|
});
|
|
152
121
|
|
|
153
|
-
await expect(
|
|
154
|
-
|
|
155
|
-
);
|
|
122
|
+
await expect(
|
|
123
|
+
oauthPostTweet("will fail", { oauthToken: "fake-oauth-token" }),
|
|
124
|
+
).rejects.toThrow(/Twitter API error \(429\)/);
|
|
156
125
|
});
|
|
157
126
|
|
|
158
|
-
test("
|
|
127
|
+
test("throws with status in error message on 401", async () => {
|
|
159
128
|
mockFetch({
|
|
160
129
|
ok: false,
|
|
161
130
|
status: 401,
|
|
@@ -163,23 +132,25 @@ describe("Twitter OAuth client", () => {
|
|
|
163
132
|
});
|
|
164
133
|
|
|
165
134
|
try {
|
|
166
|
-
await oauthPostTweet("will fail");
|
|
135
|
+
await oauthPostTweet("will fail", { oauthToken: "fake-oauth-token" });
|
|
167
136
|
expect(true).toBe(false); // should not reach
|
|
168
137
|
} catch (err) {
|
|
169
|
-
expect((err as Error
|
|
138
|
+
expect((err as Error).message).toContain("401");
|
|
170
139
|
}
|
|
171
140
|
});
|
|
172
141
|
});
|
|
173
142
|
|
|
174
143
|
describe("oauthIsAvailable", () => {
|
|
175
|
-
test("returns true when
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
144
|
+
test("returns true when token is provided", () => {
|
|
145
|
+
expect(oauthIsAvailable("some-token")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("returns false when token is undefined", () => {
|
|
149
|
+
expect(oauthIsAvailable(undefined)).toBe(false);
|
|
179
150
|
});
|
|
180
151
|
|
|
181
|
-
test("returns false when
|
|
182
|
-
expect(oauthIsAvailable()).toBe(false);
|
|
152
|
+
test("returns false when token is empty string", () => {
|
|
153
|
+
expect(oauthIsAvailable("")).toBe(false);
|
|
183
154
|
});
|
|
184
155
|
});
|
|
185
156
|
|
|
@@ -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 { createInvite, revokeInvite } from "../memory/invite-store.js";
|
|
30
31
|
import { redeemVoiceInviteCode } from "../runtime/invite-redemption-service.js";
|
|
@@ -177,6 +178,29 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
177
178
|
});
|
|
178
179
|
});
|
|
179
180
|
|
|
181
|
+
test("marks channel as verified via invite on voice redemption", () => {
|
|
182
|
+
const phone = "+15551234567";
|
|
183
|
+
const { code } = createVoiceInvite({ callerPhone: phone });
|
|
184
|
+
|
|
185
|
+
const result = redeemVoiceInviteCode({
|
|
186
|
+
callerExternalUserId: phone,
|
|
187
|
+
sourceChannel: "voice",
|
|
188
|
+
code,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(result.ok).toBe(true);
|
|
192
|
+
|
|
193
|
+
const channelResult = findContactChannel({
|
|
194
|
+
channelType: "voice",
|
|
195
|
+
externalUserId: phone,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(channelResult).not.toBeNull();
|
|
199
|
+
expect(channelResult!.channel.verifiedAt).toBeGreaterThan(0);
|
|
200
|
+
expect(channelResult!.channel.verifiedVia).toBe("invite");
|
|
201
|
+
expect(channelResult!.channel.status).toBe("active");
|
|
202
|
+
});
|
|
203
|
+
|
|
180
204
|
test("wrong caller identity fails with generic error", () => {
|
|
181
205
|
const { code } = createVoiceInvite({ callerPhone: "+15551234567" });
|
|
182
206
|
|
|
@@ -281,7 +305,7 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
281
305
|
const { code } = createVoiceInvite({ callerPhone: phone });
|
|
282
306
|
|
|
283
307
|
// Pre-create an active member for this phone on voice channel
|
|
284
|
-
|
|
308
|
+
upsertContactChannel({
|
|
285
309
|
sourceChannel: "voice",
|
|
286
310
|
externalUserId: phone,
|
|
287
311
|
status: "active",
|
|
@@ -306,7 +330,7 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
306
330
|
const phone = "+15551234567";
|
|
307
331
|
const { code } = createVoiceInvite({ callerPhone: phone });
|
|
308
332
|
|
|
309
|
-
|
|
333
|
+
upsertContactChannel({
|
|
310
334
|
sourceChannel: "voice",
|
|
311
335
|
externalUserId: phone,
|
|
312
336
|
status: "blocked",
|
package/src/amazon/cart.ts
CHANGED
|
@@ -321,7 +321,7 @@ export async function addToCart(opts: {
|
|
|
321
321
|
__error: true,
|
|
322
322
|
__message: 'Add-to-cart failed: ASIN ' + ${JSON.stringify(
|
|
323
323
|
opts.asin,
|
|
324
|
-
)} + ' was not found in the Fresh cart after adding. The item may be unavailable or the session cookies may be stale. Try running
|
|
324
|
+
)} + ' was not found in the Fresh cart after adding. The item may be unavailable or the session cookies may be stale. Try running assistant amazon refresh.'
|
|
325
325
|
});
|
|
326
326
|
}
|
|
327
327
|
items = allFreshItems.map(function(item) {
|