@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPhoneNumberSid,
|
|
3
3
|
getTollFreeVerificationStatus,
|
|
4
|
+
getTwilioCredentials,
|
|
4
5
|
hasTwilioCredentials,
|
|
5
6
|
} from "../calls/twilio-rest.js";
|
|
6
7
|
import { getChannelInvitePolicy } from "../channels/config.js";
|
|
@@ -99,9 +100,13 @@ const smsProbe: ChannelProbe = {
|
|
|
99
100
|
async runRemoteChecks(): Promise<ReadinessCheckResult[]> {
|
|
100
101
|
if (!hasTwilioCredentials()) return [];
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
let accountSid: string;
|
|
104
|
+
let authToken: string;
|
|
105
|
+
try {
|
|
106
|
+
({ accountSid, authToken } = getTwilioCredentials());
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
105
110
|
|
|
106
111
|
const phoneNumber = resolveSmsPhoneNumber();
|
|
107
112
|
if (!phoneNumber) return [];
|
|
@@ -308,7 +313,7 @@ const emailProbe: ChannelProbe = {
|
|
|
308
313
|
passed: hasInbox,
|
|
309
314
|
message: hasInbox
|
|
310
315
|
? `Inbox address is configured (${address})`
|
|
311
|
-
: "No inbox address configured — create one with:
|
|
316
|
+
: "No inbox address configured — create one with: assistant email setup inboxes",
|
|
312
317
|
},
|
|
313
318
|
];
|
|
314
319
|
} catch (err) {
|
|
@@ -30,10 +30,18 @@ async function parseErrorResponse(
|
|
|
30
30
|
let message = `Gateway request failed (${resp.status})`;
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
|
-
const parsed = JSON.parse(body) as {
|
|
33
|
+
const parsed = JSON.parse(body) as {
|
|
34
|
+
error?: string | { code?: string; message?: string };
|
|
35
|
+
};
|
|
34
36
|
if (parsed.error) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
// Runtime httpError() returns { error: { code, message } } while
|
|
38
|
+
// gateway returns { error: "string" }. Handle both formats.
|
|
39
|
+
const errStr =
|
|
40
|
+
typeof parsed.error === "string"
|
|
41
|
+
? parsed.error
|
|
42
|
+
: (parsed.error.message ?? JSON.stringify(parsed.error));
|
|
43
|
+
gatewayError = errStr;
|
|
44
|
+
message = errStr;
|
|
37
45
|
}
|
|
38
46
|
} catch {
|
|
39
47
|
if (body) message = body;
|
|
@@ -126,6 +126,7 @@ import {
|
|
|
126
126
|
pairingRouteDefinitions,
|
|
127
127
|
} from "./routes/pairing-routes.js";
|
|
128
128
|
import { secretRouteDefinitions } from "./routes/secret-routes.js";
|
|
129
|
+
import { slackShareRouteDefinitions } from "./routes/slack-share-routes.js";
|
|
129
130
|
import { surfaceActionRouteDefinitions } from "./routes/surface-action-routes.js";
|
|
130
131
|
import { surfaceContentRouteDefinitions } from "./routes/surface-content-routes.js";
|
|
131
132
|
import { trustRulesRouteDefinitions } from "./routes/trust-rules-routes.js";
|
|
@@ -851,6 +852,7 @@ export class RuntimeHttpServer {
|
|
|
851
852
|
...contactCatchAllRouteDefinitions(),
|
|
852
853
|
|
|
853
854
|
...integrationRouteDefinitions(),
|
|
855
|
+
...slackShareRouteDefinitions(),
|
|
854
856
|
...twilioRouteDefinitions(),
|
|
855
857
|
...channelReadinessRouteDefinitions(),
|
|
856
858
|
...attachmentRouteDefinitions(),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ChannelId } from "../channels/types.js";
|
|
11
11
|
import { findContactChannel } from "../contacts/contact-store.js";
|
|
12
|
-
import {
|
|
12
|
+
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
13
13
|
import { getSqlite } from "../memory/db.js";
|
|
14
14
|
import {
|
|
15
15
|
findActiveVoiceInvites,
|
|
@@ -147,9 +147,9 @@ export function redeemInvite(params: {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// Inactive member reactivation: when the user already has a member record
|
|
150
|
-
// in a non-active state (revoked/pending), reactivate it via
|
|
150
|
+
// in a non-active state (revoked/pending), reactivate it via upsertContactChannel
|
|
151
151
|
// and consume an invite use atomically. The fresh-member path below also
|
|
152
|
-
// uses
|
|
152
|
+
// uses upsertContactChannel to keep contacts in sync.
|
|
153
153
|
if (existingChannel) {
|
|
154
154
|
// Sentinel error used to trigger a transaction rollback when the invite
|
|
155
155
|
// was concurrently revoked/expired between pre-validation and write time.
|
|
@@ -173,11 +173,11 @@ export function redeemInvite(params: {
|
|
|
173
173
|
? existingContact.displayName
|
|
174
174
|
: displayName;
|
|
175
175
|
|
|
176
|
-
let reactivated: ReturnType<typeof
|
|
176
|
+
let reactivated: ReturnType<typeof upsertContactChannel> | undefined;
|
|
177
177
|
try {
|
|
178
178
|
getSqlite()
|
|
179
179
|
.transaction(() => {
|
|
180
|
-
reactivated =
|
|
180
|
+
reactivated = upsertContactChannel({
|
|
181
181
|
sourceChannel,
|
|
182
182
|
externalUserId,
|
|
183
183
|
externalChatId,
|
|
@@ -187,6 +187,8 @@ export function redeemInvite(params: {
|
|
|
187
187
|
status: "active",
|
|
188
188
|
policy: "allow",
|
|
189
189
|
inviteId: invite.id,
|
|
190
|
+
verifiedAt: Date.now(),
|
|
191
|
+
verifiedVia: "invite",
|
|
190
192
|
});
|
|
191
193
|
|
|
192
194
|
const recorded = recordInviteUse({
|
|
@@ -219,11 +221,11 @@ export function redeemInvite(params: {
|
|
|
219
221
|
// Fresh member creation: upsert into contacts tables and consume an invite
|
|
220
222
|
// use atomically, mirroring the reactivation path above.
|
|
221
223
|
const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
|
|
222
|
-
let freshResult: ReturnType<typeof
|
|
224
|
+
let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
|
|
223
225
|
try {
|
|
224
226
|
getSqlite()
|
|
225
227
|
.transaction(() => {
|
|
226
|
-
freshResult =
|
|
228
|
+
freshResult = upsertContactChannel({
|
|
227
229
|
sourceChannel,
|
|
228
230
|
externalUserId,
|
|
229
231
|
externalChatId,
|
|
@@ -232,6 +234,8 @@ export function redeemInvite(params: {
|
|
|
232
234
|
status: "active",
|
|
233
235
|
policy: "allow",
|
|
234
236
|
inviteId: invite.id,
|
|
237
|
+
verifiedAt: Date.now(),
|
|
238
|
+
verifiedVia: "invite",
|
|
235
239
|
});
|
|
236
240
|
|
|
237
241
|
const recorded = recordInviteUse({
|
|
@@ -362,7 +366,7 @@ export function redeemVoiceInviteCode(params: {
|
|
|
362
366
|
try {
|
|
363
367
|
getSqlite()
|
|
364
368
|
.transaction(() => {
|
|
365
|
-
const writeResult =
|
|
369
|
+
const writeResult = upsertContactChannel({
|
|
366
370
|
sourceChannel: "voice",
|
|
367
371
|
externalUserId: callerExternalUserId,
|
|
368
372
|
externalChatId: callerExternalUserId,
|
|
@@ -370,6 +374,8 @@ export function redeemVoiceInviteCode(params: {
|
|
|
370
374
|
status: "active",
|
|
371
375
|
policy: "allow",
|
|
372
376
|
inviteId: invite.id,
|
|
377
|
+
verifiedAt: Date.now(),
|
|
378
|
+
verifiedVia: "invite",
|
|
373
379
|
});
|
|
374
380
|
memberId = writeResult!.channel.id;
|
|
375
381
|
|
|
@@ -481,7 +487,7 @@ export function redeemInviteByCode(params: {
|
|
|
481
487
|
return { ok: false, reason: "invalid_token" };
|
|
482
488
|
}
|
|
483
489
|
|
|
484
|
-
// Inactive member reactivation: reactivate via
|
|
490
|
+
// Inactive member reactivation: reactivate via upsertContactChannel and consume
|
|
485
491
|
// an invite use atomically.
|
|
486
492
|
if (existingChannel) {
|
|
487
493
|
const STALE_INVITE_REACTIVATE = Symbol("stale_invite_reactivate");
|
|
@@ -504,11 +510,11 @@ export function redeemInviteByCode(params: {
|
|
|
504
510
|
? existingContact.displayName
|
|
505
511
|
: displayName;
|
|
506
512
|
|
|
507
|
-
let reactivated: ReturnType<typeof
|
|
513
|
+
let reactivated: ReturnType<typeof upsertContactChannel> | undefined;
|
|
508
514
|
try {
|
|
509
515
|
getSqlite()
|
|
510
516
|
.transaction(() => {
|
|
511
|
-
reactivated =
|
|
517
|
+
reactivated = upsertContactChannel({
|
|
512
518
|
sourceChannel,
|
|
513
519
|
externalUserId,
|
|
514
520
|
externalChatId,
|
|
@@ -517,6 +523,8 @@ export function redeemInviteByCode(params: {
|
|
|
517
523
|
status: "active",
|
|
518
524
|
policy: "allow",
|
|
519
525
|
inviteId: invite.id,
|
|
526
|
+
verifiedAt: Date.now(),
|
|
527
|
+
verifiedVia: "invite",
|
|
520
528
|
});
|
|
521
529
|
|
|
522
530
|
const recorded = recordInviteUse({
|
|
@@ -546,11 +554,11 @@ export function redeemInviteByCode(params: {
|
|
|
546
554
|
// Fresh member creation: upsert into contacts tables and consume an invite
|
|
547
555
|
// use atomically.
|
|
548
556
|
const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
|
|
549
|
-
let freshResult: ReturnType<typeof
|
|
557
|
+
let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
|
|
550
558
|
try {
|
|
551
559
|
getSqlite()
|
|
552
560
|
.transaction(() => {
|
|
553
|
-
freshResult =
|
|
561
|
+
freshResult = upsertContactChannel({
|
|
554
562
|
sourceChannel,
|
|
555
563
|
externalUserId,
|
|
556
564
|
externalChatId,
|
|
@@ -559,6 +567,8 @@ export function redeemInviteByCode(params: {
|
|
|
559
567
|
status: "active",
|
|
560
568
|
policy: "allow",
|
|
561
569
|
inviteId: invite.id,
|
|
570
|
+
verifiedAt: Date.now(),
|
|
571
|
+
verifiedVia: "invite",
|
|
562
572
|
});
|
|
563
573
|
|
|
564
574
|
const recorded = recordInviteUse({
|
|
@@ -71,10 +71,10 @@ export async function validateTwilioWebhook(
|
|
|
71
71
|
|
|
72
72
|
const authToken = TwilioConversationRelayProvider.getAuthToken();
|
|
73
73
|
|
|
74
|
-
// Fail-closed: reject if no auth token is configured
|
|
75
74
|
if (!authToken) {
|
|
76
75
|
log.error(
|
|
77
|
-
"Twilio auth token not
|
|
76
|
+
"Twilio auth token not found in secure key store — cannot verify webhook HMAC signature. " +
|
|
77
|
+
"Rejecting request. Set credential:twilio:auth_token via the credential_store tool.",
|
|
78
78
|
);
|
|
79
79
|
return httpError("FORBIDDEN", "Forbidden", 403);
|
|
80
80
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Route handlers for shareable app pages and cloud sharing.
|
|
3
3
|
*/
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { extname, join } from "node:path";
|
|
7
7
|
|
|
8
8
|
import JSZip from "jszip";
|
|
9
9
|
|
|
10
10
|
import type { AppManifest } from "../../bundler/manifest.js";
|
|
11
|
-
import { getApp } from "../../memory/app-store.js";
|
|
11
|
+
import { getApp, getAppsDir, isMultifileApp } from "../../memory/app-store.js";
|
|
12
12
|
import * as sharedAppLinksStore from "../../memory/shared-app-links-store.js";
|
|
13
13
|
import { getLogger } from "../../util/logger.js";
|
|
14
14
|
import { httpError } from "../http-errors.js";
|
|
@@ -46,6 +46,11 @@ export function handleServePage(appId: string): Response {
|
|
|
46
46
|
return httpError("NOT_FOUND", "App not found", 404);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Multifile apps serve the compiled dist/index.html directly.
|
|
50
|
+
if (isMultifileApp(app)) {
|
|
51
|
+
return serveMultifileApp(appId, app.name);
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
const css = loadDesignSystemCss();
|
|
50
55
|
const escapedName = app.name.replace(
|
|
51
56
|
/[<>&"]/g,
|
|
@@ -99,6 +104,122 @@ ${noncedHtml}
|
|
|
99
104
|
});
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Serve compiled output for multifile TSX apps.
|
|
109
|
+
* Falls back to a "not compiled yet" message if dist/index.html is missing.
|
|
110
|
+
*/
|
|
111
|
+
function serveMultifileApp(appId: string, appName: string): Response {
|
|
112
|
+
const distDir = join(getAppsDir(), appId, "dist");
|
|
113
|
+
const indexPath = join(distDir, "index.html");
|
|
114
|
+
|
|
115
|
+
if (!existsSync(indexPath)) {
|
|
116
|
+
const escapedName = appName.replace(
|
|
117
|
+
/[<>&"]/g,
|
|
118
|
+
(c) => HTML_ESCAPE_MAP[c] ?? c,
|
|
119
|
+
);
|
|
120
|
+
return new Response(
|
|
121
|
+
`<!DOCTYPE html><html><head><title>${escapedName}</title></head>` +
|
|
122
|
+
`<body><p>App has not been compiled yet. Edit a source file to trigger a build.</p></body></html>`,
|
|
123
|
+
{
|
|
124
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Rewrite relative asset paths to absolute HTTP routes so browsers and
|
|
130
|
+
// HTTP-based consumers (e.g. /pages/:appId) can resolve them. The macOS
|
|
131
|
+
// WebView uses the vellumapp:// scheme handler which resolves on disk,
|
|
132
|
+
// but HTTP clients need the /v1/apps/:appId/dist/ route.
|
|
133
|
+
let html = readFileSync(indexPath, "utf-8");
|
|
134
|
+
html = html.replace(
|
|
135
|
+
/(?:src|href)="(\.?\/?main\.(js|css))"/g,
|
|
136
|
+
(_match, _filename, ext) => {
|
|
137
|
+
const attr = ext === "css" ? "href" : "src";
|
|
138
|
+
return `${attr}="/v1/apps/${appId}/dist/main.${ext}"`;
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Compiled apps use external scripts so 'unsafe-inline' is not needed for
|
|
143
|
+
// script-src; however we keep it for style-src since the app HTML may use
|
|
144
|
+
// inline styles.
|
|
145
|
+
const csp = [
|
|
146
|
+
"default-src 'self'",
|
|
147
|
+
"style-src 'self' 'unsafe-inline'",
|
|
148
|
+
"script-src 'self'",
|
|
149
|
+
"img-src 'self' data: https:",
|
|
150
|
+
"font-src 'self' data: https:",
|
|
151
|
+
"object-src 'none'",
|
|
152
|
+
"base-uri 'self'",
|
|
153
|
+
"form-action 'self'",
|
|
154
|
+
"frame-ancestors 'self'",
|
|
155
|
+
].join("; ");
|
|
156
|
+
|
|
157
|
+
return new Response(html, {
|
|
158
|
+
headers: {
|
|
159
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
160
|
+
"Content-Security-Policy": csp,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Content-Type map for static dist/ assets. */
|
|
166
|
+
const DIST_CONTENT_TYPES: Record<string, string> = {
|
|
167
|
+
".js": "application/javascript",
|
|
168
|
+
".css": "text/css",
|
|
169
|
+
".html": "text/html",
|
|
170
|
+
".json": "application/json",
|
|
171
|
+
".svg": "image/svg+xml",
|
|
172
|
+
".png": "image/png",
|
|
173
|
+
".jpg": "image/jpeg",
|
|
174
|
+
".jpeg": "image/jpeg",
|
|
175
|
+
".woff2": "font/woff2",
|
|
176
|
+
".woff": "font/woff",
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Serve a static file from an app's dist/ directory.
|
|
181
|
+
* Validates the filename to prevent path traversal.
|
|
182
|
+
*/
|
|
183
|
+
export function handleServeDistFile(appId: string, filename: string): Response {
|
|
184
|
+
// Reject any traversal attempts on appId
|
|
185
|
+
if (
|
|
186
|
+
!appId ||
|
|
187
|
+
appId.includes("..") ||
|
|
188
|
+
appId.includes("/") ||
|
|
189
|
+
appId.includes("\\") ||
|
|
190
|
+
appId !== appId.trim()
|
|
191
|
+
) {
|
|
192
|
+
return httpError("BAD_REQUEST", "Invalid appId", 400);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Reject any traversal attempts on filename
|
|
196
|
+
if (
|
|
197
|
+
!filename ||
|
|
198
|
+
filename.includes("..") ||
|
|
199
|
+
filename.includes("/") ||
|
|
200
|
+
filename.includes("\\") ||
|
|
201
|
+
filename !== filename.trim()
|
|
202
|
+
) {
|
|
203
|
+
return httpError("BAD_REQUEST", "Invalid filename", 400);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const filePath = join(getAppsDir(), appId, "dist", filename);
|
|
207
|
+
if (!existsSync(filePath)) {
|
|
208
|
+
return httpError("NOT_FOUND", "File not found", 404);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ext = extname(filename).toLowerCase();
|
|
212
|
+
const contentType = DIST_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
213
|
+
const content = readFileSync(filePath);
|
|
214
|
+
|
|
215
|
+
return new Response(content, {
|
|
216
|
+
headers: {
|
|
217
|
+
"Content-Type": contentType,
|
|
218
|
+
"Cache-Control": "no-cache",
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
102
223
|
/** 50 MB — generous cap for zip app bundles. */
|
|
103
224
|
const MAX_SHARE_BODY_BYTES = 50 * 1024 * 1024;
|
|
104
225
|
|
|
@@ -207,6 +328,13 @@ export function handleDeleteSharedApp(shareToken: string): Response {
|
|
|
207
328
|
|
|
208
329
|
export function appRouteDefinitions(): RouteDefinition[] {
|
|
209
330
|
return [
|
|
331
|
+
{
|
|
332
|
+
endpoint: "apps/:appId/dist/:filename",
|
|
333
|
+
method: "GET",
|
|
334
|
+
policyKey: "apps/dist",
|
|
335
|
+
handler: ({ params }) =>
|
|
336
|
+
handleServeDistFile(params.appId, params.filename),
|
|
337
|
+
},
|
|
210
338
|
{
|
|
211
339
|
endpoint: "apps/share",
|
|
212
340
|
method: "POST",
|
|
@@ -18,7 +18,7 @@ import { findContactChannel } from "../../../contacts/contact-store.js";
|
|
|
18
18
|
import {
|
|
19
19
|
createGuardianBinding,
|
|
20
20
|
revokeGuardianBinding,
|
|
21
|
-
|
|
21
|
+
upsertContactChannel,
|
|
22
22
|
} from "../../../contacts/contacts-write.js";
|
|
23
23
|
import * as channelDeliveryStore from "../../../memory/channel-delivery-store.js";
|
|
24
24
|
import { emitNotificationSignal } from "../../../notifications/emit-signal.js";
|
|
@@ -143,7 +143,7 @@ export async function handleVerificationIntercept(
|
|
|
143
143
|
? existingContact.displayName
|
|
144
144
|
: actorDisplayName;
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
upsertContactChannel({
|
|
147
147
|
sourceChannel,
|
|
148
148
|
externalUserId: canonicalSenderId ?? rawSenderId,
|
|
149
149
|
externalChatId: conversationExternalId,
|
|
@@ -169,7 +169,7 @@ export async function handleVerificationIntercept(
|
|
|
169
169
|
(canonicalSenderId ?? rawSenderId)
|
|
170
170
|
) {
|
|
171
171
|
// Edge case: another user already bound. Log and skip binding creation.
|
|
172
|
-
// The
|
|
172
|
+
// The upsertContactChannel above already succeeded, so the sender is a known contact,
|
|
173
173
|
// but they won't get guardian role.
|
|
174
174
|
log.warn(
|
|
175
175
|
{
|
|
@@ -139,8 +139,8 @@ export async function handleSetSlackChannelConfig(
|
|
|
139
139
|
/**
|
|
140
140
|
* DELETE /v1/integrations/slack/channel/config
|
|
141
141
|
*/
|
|
142
|
-
export function handleClearSlackChannelConfig(): Response {
|
|
143
|
-
const result = clearSlackChannelConfig();
|
|
142
|
+
export async function handleClearSlackChannelConfig(): Promise<Response> {
|
|
143
|
+
const result = await clearSlackChannelConfig();
|
|
144
144
|
return Response.json(result);
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handlers for Slack channel listing and direct sharing.
|
|
3
|
+
*
|
|
4
|
+
* These endpoints let the UI post app links directly to Slack channels
|
|
5
|
+
* without going through the legacy IPC-based Slack share flow.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getApp } from "../../memory/app-store.js";
|
|
9
|
+
import {
|
|
10
|
+
listConversations,
|
|
11
|
+
postMessage,
|
|
12
|
+
userInfo,
|
|
13
|
+
} from "../../messaging/providers/slack/client.js";
|
|
14
|
+
import type { SlackConversation } from "../../messaging/providers/slack/types.js";
|
|
15
|
+
import { getSecureKey } from "../../security/secure-keys.js";
|
|
16
|
+
import { getLogger } from "../../util/logger.js";
|
|
17
|
+
import { httpError } from "../http-errors.js";
|
|
18
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
19
|
+
|
|
20
|
+
const log = getLogger("slack-share-routes");
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Shared helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the Slack bot token from secure storage.
|
|
28
|
+
* Prefers the OAuth integration token, falls back to the legacy channel token.
|
|
29
|
+
*/
|
|
30
|
+
function resolveSlackToken(): string | undefined {
|
|
31
|
+
return (
|
|
32
|
+
getSecureKey("credential:integration:slack:access_token") ??
|
|
33
|
+
getSecureKey("credential:slack_channel:bot_token")
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// GET /v1/slack/channels
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
interface NormalizedChannel {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
type: "channel" | "group" | "dm";
|
|
45
|
+
isPrivate: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function classifyConversation(
|
|
49
|
+
conv: SlackConversation,
|
|
50
|
+
): "channel" | "group" | "dm" {
|
|
51
|
+
if (conv.is_im) return "dm";
|
|
52
|
+
if (conv.is_mpim) return "group";
|
|
53
|
+
if (conv.is_group) return "group";
|
|
54
|
+
return "channel";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const TYPE_SORT_ORDER: Record<string, number> = {
|
|
58
|
+
channel: 0,
|
|
59
|
+
group: 1,
|
|
60
|
+
dm: 2,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export async function handleListSlackChannels(): Promise<Response> {
|
|
64
|
+
const token = resolveSlackToken();
|
|
65
|
+
if (!token) {
|
|
66
|
+
return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Paginate through all results (follows the pattern in adapter.ts)
|
|
70
|
+
const allChannels: SlackConversation[] = [];
|
|
71
|
+
let cursor: string | undefined;
|
|
72
|
+
do {
|
|
73
|
+
const resp = await listConversations(
|
|
74
|
+
token,
|
|
75
|
+
"public_channel,private_channel,mpim,im",
|
|
76
|
+
true,
|
|
77
|
+
200,
|
|
78
|
+
cursor,
|
|
79
|
+
);
|
|
80
|
+
allChannels.push(...resp.channels);
|
|
81
|
+
cursor = resp.response_metadata?.next_cursor || undefined;
|
|
82
|
+
} while (cursor);
|
|
83
|
+
|
|
84
|
+
// Resolve DM display names in parallel, tolerating individual failures.
|
|
85
|
+
const dmUserIds = allChannels
|
|
86
|
+
.filter((c) => c.is_im && c.user)
|
|
87
|
+
.map((c) => c.user!);
|
|
88
|
+
const uniqueUserIds = [...new Set(dmUserIds)];
|
|
89
|
+
const nameResults = await Promise.allSettled(
|
|
90
|
+
uniqueUserIds.map((uid) =>
|
|
91
|
+
userInfo(token, uid).then((r) => ({
|
|
92
|
+
uid,
|
|
93
|
+
name:
|
|
94
|
+
r.user.profile?.display_name ||
|
|
95
|
+
r.user.profile?.real_name ||
|
|
96
|
+
r.user.real_name ||
|
|
97
|
+
r.user.name,
|
|
98
|
+
})),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
const nameMap = new Map<string, string>();
|
|
102
|
+
for (const r of nameResults) {
|
|
103
|
+
if (r.status === "fulfilled") {
|
|
104
|
+
nameMap.set(r.value.uid, r.value.name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const channels: NormalizedChannel[] = allChannels.map((c) => {
|
|
109
|
+
const type = classifyConversation(c);
|
|
110
|
+
let name = c.name ?? c.id;
|
|
111
|
+
if (type === "dm" && c.user) {
|
|
112
|
+
name = nameMap.get(c.user) ?? c.user;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
id: c.id,
|
|
116
|
+
name,
|
|
117
|
+
type,
|
|
118
|
+
isPrivate: c.is_private ?? c.is_group ?? false,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Sort: channels first, then groups, then DMs — alphabetical within each.
|
|
123
|
+
channels.sort((a, b) => {
|
|
124
|
+
const typeOrder =
|
|
125
|
+
(TYPE_SORT_ORDER[a.type] ?? 9) - (TYPE_SORT_ORDER[b.type] ?? 9);
|
|
126
|
+
if (typeOrder !== 0) return typeOrder;
|
|
127
|
+
return a.name.localeCompare(b.name);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return Response.json({ channels });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// POST /v1/slack/share
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export async function handleShareToSlackChannel(
|
|
138
|
+
req: Request,
|
|
139
|
+
): Promise<Response> {
|
|
140
|
+
const token = resolveSlackToken();
|
|
141
|
+
if (!token) {
|
|
142
|
+
return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let body: Record<string, unknown>;
|
|
146
|
+
try {
|
|
147
|
+
body = (await req.json()) as Record<string, unknown>;
|
|
148
|
+
} catch {
|
|
149
|
+
return httpError("BAD_REQUEST", "Malformed JSON body", 400);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const appId = body.appId;
|
|
153
|
+
const channelId = body.channelId;
|
|
154
|
+
const message = body.message;
|
|
155
|
+
|
|
156
|
+
if (!appId || !channelId) {
|
|
157
|
+
return httpError(
|
|
158
|
+
"BAD_REQUEST",
|
|
159
|
+
"Missing required fields: appId, channelId",
|
|
160
|
+
400,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof appId !== "string" || typeof channelId !== "string") {
|
|
165
|
+
return httpError(
|
|
166
|
+
"BAD_REQUEST",
|
|
167
|
+
"Fields appId and channelId must be strings",
|
|
168
|
+
400,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (message !== undefined && typeof message !== "string") {
|
|
173
|
+
return httpError("BAD_REQUEST", "Field message must be a string", 400);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const app = getApp(appId);
|
|
177
|
+
if (!app) {
|
|
178
|
+
return httpError("NOT_FOUND", "App not found", 404);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build a Block Kit message with a deterministic fallback text.
|
|
182
|
+
const fallbackText = message
|
|
183
|
+
? `${message} — ${app.name}`
|
|
184
|
+
: `Shared app: ${app.name}`;
|
|
185
|
+
|
|
186
|
+
const blocks: unknown[] = [
|
|
187
|
+
{
|
|
188
|
+
type: "section",
|
|
189
|
+
text: {
|
|
190
|
+
type: "mrkdwn",
|
|
191
|
+
text: message ? `${message}\n\n*${app.name}*` : `*${app.name}*`,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
if (app.description) {
|
|
197
|
+
blocks.push({
|
|
198
|
+
type: "context",
|
|
199
|
+
elements: [{ type: "mrkdwn", text: app.description }],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const result = await postMessage(token, channelId, fallbackText, {
|
|
205
|
+
blocks,
|
|
206
|
+
});
|
|
207
|
+
return Response.json({
|
|
208
|
+
ok: true,
|
|
209
|
+
ts: result.ts,
|
|
210
|
+
channel: result.channel,
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
log.error({ err, appId, channelId }, "Failed to share app to Slack");
|
|
214
|
+
return httpError("INTERNAL_ERROR", "Failed to post message to Slack", 500);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Route definitions
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
export function slackShareRouteDefinitions(): RouteDefinition[] {
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
endpoint: "slack/channels",
|
|
226
|
+
method: "GET",
|
|
227
|
+
handler: () => handleListSlackChannels(),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
endpoint: "slack/share",
|
|
231
|
+
method: "POST",
|
|
232
|
+
handler: async ({ req }) => handleShareToSlackChannel(req),
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
}
|