@vellumai/assistant 0.5.11 → 0.5.13
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/Dockerfile +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -447,10 +447,11 @@ export async function enforceIngressAcl(
|
|
|
447
447
|
);
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
const replyText = guardianNotified
|
|
451
|
+
? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
|
|
452
|
+
: "Sorry, you haven't been approved to message this assistant.";
|
|
453
|
+
let replyDelivered = false;
|
|
450
454
|
if (replyCallbackUrl) {
|
|
451
|
-
const replyText = guardianNotified
|
|
452
|
-
? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
|
|
453
|
-
: "Sorry, you haven't been approved to message this assistant.";
|
|
454
455
|
const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
455
456
|
chatId: conversationExternalId,
|
|
456
457
|
text: replyText,
|
|
@@ -467,6 +468,7 @@ export async function enforceIngressAcl(
|
|
|
467
468
|
replyPayload,
|
|
468
469
|
mintBearerToken(),
|
|
469
470
|
);
|
|
471
|
+
replyDelivered = true;
|
|
470
472
|
} catch (err) {
|
|
471
473
|
log.error(
|
|
472
474
|
{ err, conversationExternalId },
|
|
@@ -481,6 +483,9 @@ export async function enforceIngressAcl(
|
|
|
481
483
|
accepted: true,
|
|
482
484
|
denied: true,
|
|
483
485
|
reason: "not_a_member",
|
|
486
|
+
// Include reply text so the gateway can deliver directly when
|
|
487
|
+
// callback delivery failed (e.g. signing-key mismatch → 401).
|
|
488
|
+
...(!replyDelivered && { replyText }),
|
|
484
489
|
}),
|
|
485
490
|
guardianVerifyCode,
|
|
486
491
|
};
|
|
@@ -714,15 +719,16 @@ export async function enforceIngressAcl(
|
|
|
714
719
|
}
|
|
715
720
|
}
|
|
716
721
|
|
|
722
|
+
const inactiveReplyText = guardianNotified
|
|
723
|
+
? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
|
|
724
|
+
: "Sorry, you haven't been approved to message this assistant.";
|
|
725
|
+
let inactiveReplyDelivered = false;
|
|
717
726
|
if (replyCallbackUrl) {
|
|
718
|
-
const replyText = guardianNotified
|
|
719
|
-
? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
|
|
720
|
-
: "Sorry, you haven't been approved to message this assistant.";
|
|
721
727
|
const inactiveReplyPayload: Parameters<
|
|
722
728
|
typeof deliverChannelReply
|
|
723
729
|
>[1] = {
|
|
724
730
|
chatId: conversationExternalId,
|
|
725
|
-
text:
|
|
731
|
+
text: inactiveReplyText,
|
|
726
732
|
assistantId,
|
|
727
733
|
};
|
|
728
734
|
// On Slack, send as ephemeral so only the requester sees the rejection
|
|
@@ -739,6 +745,7 @@ export async function enforceIngressAcl(
|
|
|
739
745
|
inactiveReplyPayload,
|
|
740
746
|
mintBearerToken(),
|
|
741
747
|
);
|
|
748
|
+
inactiveReplyDelivered = true;
|
|
742
749
|
} catch (err) {
|
|
743
750
|
log.error(
|
|
744
751
|
{ err, conversationExternalId },
|
|
@@ -752,6 +759,7 @@ export async function enforceIngressAcl(
|
|
|
752
759
|
accepted: true,
|
|
753
760
|
denied: true,
|
|
754
761
|
reason: `member_${channelStatusToMemberStatus(resolvedMember.channel.status)}`,
|
|
762
|
+
...(!inactiveReplyDelivered && { replyText: inactiveReplyText }),
|
|
755
763
|
}),
|
|
756
764
|
guardianVerifyCode,
|
|
757
765
|
};
|
|
@@ -763,10 +771,13 @@ export async function enforceIngressAcl(
|
|
|
763
771
|
{ sourceChannel, channelId: resolvedMember.channel.id },
|
|
764
772
|
"Ingress ACL: member policy deny",
|
|
765
773
|
);
|
|
774
|
+
const denyReplyText =
|
|
775
|
+
"Sorry, you haven't been approved to message this assistant.";
|
|
776
|
+
let denyReplyDelivered = false;
|
|
766
777
|
if (replyCallbackUrl) {
|
|
767
778
|
const denyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
768
779
|
chatId: conversationExternalId,
|
|
769
|
-
text:
|
|
780
|
+
text: denyReplyText,
|
|
770
781
|
assistantId,
|
|
771
782
|
};
|
|
772
783
|
if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
|
|
@@ -779,6 +790,7 @@ export async function enforceIngressAcl(
|
|
|
779
790
|
denyPayload,
|
|
780
791
|
mintBearerToken(),
|
|
781
792
|
);
|
|
793
|
+
denyReplyDelivered = true;
|
|
782
794
|
} catch (err) {
|
|
783
795
|
log.error(
|
|
784
796
|
{ err, conversationExternalId },
|
|
@@ -792,6 +804,7 @@ export async function enforceIngressAcl(
|
|
|
792
804
|
accepted: true,
|
|
793
805
|
denied: true,
|
|
794
806
|
reason: "policy_deny",
|
|
807
|
+
...(!denyReplyDelivered && { replyText: denyReplyText }),
|
|
795
808
|
}),
|
|
796
809
|
guardianVerifyCode,
|
|
797
810
|
};
|
|
@@ -28,7 +28,7 @@ const log = getLogger("slack-share");
|
|
|
28
28
|
* Resolve the Slack bot token from the OAuth connection store.
|
|
29
29
|
*/
|
|
30
30
|
async function resolveSlackToken(): Promise<string | undefined> {
|
|
31
|
-
const conn = getConnectionByProvider("
|
|
31
|
+
const conn = getConnectionByProvider("slack");
|
|
32
32
|
return conn
|
|
33
33
|
? await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`)
|
|
34
34
|
: undefined;
|
|
@@ -81,7 +81,8 @@ export function oauthAppsRouteDefinitions(): RouteDefinition[] {
|
|
|
81
81
|
description: providerRow.description ?? null,
|
|
82
82
|
dashboard_url: providerRow.dashboardUrl ?? null,
|
|
83
83
|
client_id_placeholder: providerRow.clientIdPlaceholder ?? null,
|
|
84
|
-
requires_client_secret: providerRow.requiresClientSecret ?? 1,
|
|
84
|
+
requires_client_secret: !!(providerRow.requiresClientSecret ?? 1),
|
|
85
|
+
supports_managed_mode: !!providerRow.managedServiceConfigKey,
|
|
85
86
|
}
|
|
86
87
|
: null;
|
|
87
88
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
getPlatformAssistantId,
|
|
4
5
|
setPlatformAssistantId,
|
|
5
6
|
setPlatformBaseUrl,
|
|
6
7
|
setPlatformOrganizationId,
|
|
@@ -86,7 +87,10 @@ async function queueApiKeyPropagation(
|
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
try {
|
|
89
|
-
await cesClient.updateAssistantApiKey(
|
|
90
|
+
await cesClient.updateAssistantApiKey(
|
|
91
|
+
apiKey,
|
|
92
|
+
getPlatformAssistantId() || undefined,
|
|
93
|
+
);
|
|
90
94
|
log.info(
|
|
91
95
|
"Pushed queued assistant API key to CES after handshake completed",
|
|
92
96
|
);
|
|
@@ -106,7 +110,7 @@ async function queueApiKeyPropagation(
|
|
|
106
110
|
|
|
107
111
|
export async function handleAddSecret(
|
|
108
112
|
req: Request,
|
|
109
|
-
|
|
113
|
+
deps?: SecretRouteDeps,
|
|
110
114
|
): Promise<Response> {
|
|
111
115
|
let body: { type?: string; name?: string; value?: string };
|
|
112
116
|
try {
|
|
@@ -192,9 +196,7 @@ export async function handleAddSecret(
|
|
|
192
196
|
500,
|
|
193
197
|
);
|
|
194
198
|
}
|
|
195
|
-
|
|
196
|
-
invalidateConfigCache();
|
|
197
|
-
await initializeProviders(getConfig());
|
|
199
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
198
200
|
log.info({ provider: name }, "API key updated via HTTP");
|
|
199
201
|
return Response.json({ success: true, type, name }, { status: 201 });
|
|
200
202
|
}
|
|
@@ -275,16 +277,19 @@ export async function handleAddSecret(
|
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
if (isManagedProxyCredential(service, field)) {
|
|
278
|
-
await
|
|
280
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
279
281
|
if (service === "vellum" && field === "assistant_api_key") {
|
|
280
282
|
// Push the API key to CES so managed credential materialization
|
|
281
283
|
// works even though the handshake ran before the key was available.
|
|
282
284
|
const generation = ++apiKeyGeneration;
|
|
283
|
-
const cesClient = getCesClient?.();
|
|
285
|
+
const cesClient = deps?.getCesClient?.();
|
|
284
286
|
if (cesClient) {
|
|
285
287
|
if (cesClient.isReady()) {
|
|
286
288
|
try {
|
|
287
|
-
await cesClient.updateAssistantApiKey(
|
|
289
|
+
await cesClient.updateAssistantApiKey(
|
|
290
|
+
value,
|
|
291
|
+
getPlatformAssistantId() || undefined,
|
|
292
|
+
);
|
|
288
293
|
log.info(
|
|
289
294
|
"Pushed assistant API key to CES after managed proxy credential update",
|
|
290
295
|
);
|
|
@@ -394,7 +399,10 @@ export async function handleReadSecret(req: Request): Promise<Response> {
|
|
|
394
399
|
}
|
|
395
400
|
}
|
|
396
401
|
|
|
397
|
-
export async function handleDeleteSecret(
|
|
402
|
+
export async function handleDeleteSecret(
|
|
403
|
+
req: Request,
|
|
404
|
+
deps?: SecretRouteDeps,
|
|
405
|
+
): Promise<Response> {
|
|
398
406
|
let body: { type?: string; name?: string };
|
|
399
407
|
try {
|
|
400
408
|
body = (await req.json()) as { type?: string; name?: string };
|
|
@@ -438,9 +446,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
438
446
|
500,
|
|
439
447
|
);
|
|
440
448
|
}
|
|
441
|
-
|
|
442
|
-
invalidateConfigCache();
|
|
443
|
-
await initializeProviders(getConfig());
|
|
449
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
444
450
|
log.info({ provider: name }, "API key deleted via HTTP");
|
|
445
451
|
return Response.json({ success: true, type, name });
|
|
446
452
|
}
|
|
@@ -488,7 +494,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
488
494
|
setSentryUserId(undefined);
|
|
489
495
|
}
|
|
490
496
|
if (isManagedProxyCredential(service, field)) {
|
|
491
|
-
await
|
|
497
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
492
498
|
}
|
|
493
499
|
log.info({ service, field }, "Credential deleted via HTTP");
|
|
494
500
|
return Response.json({ success: true, type, name });
|
|
@@ -548,6 +554,30 @@ export async function handleListSecrets(): Promise<Response> {
|
|
|
548
554
|
export interface SecretRouteDeps {
|
|
549
555
|
/** Accessor for the CES client, used to push API key updates after hatch. */
|
|
550
556
|
getCesClient?: () => CesClient | undefined;
|
|
557
|
+
/**
|
|
558
|
+
* Called after provider-affecting credentials change so live conversations
|
|
559
|
+
* can be reloaded with fresh provider instances.
|
|
560
|
+
*/
|
|
561
|
+
onProviderCredentialsChanged?: () => void | Promise<void>;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function refreshProvidersAfterSecretChange(
|
|
565
|
+
deps?: SecretRouteDeps,
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
clearEmbeddingBackendCache();
|
|
568
|
+
invalidateConfigCache();
|
|
569
|
+
await initializeProviders(getConfig());
|
|
570
|
+
|
|
571
|
+
if (!deps?.onProviderCredentialsChanged) return;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
await deps.onProviderCredentialsChanged();
|
|
575
|
+
} catch (err) {
|
|
576
|
+
log.warn(
|
|
577
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
578
|
+
"Failed to refresh live conversations after provider credential change",
|
|
579
|
+
);
|
|
580
|
+
}
|
|
551
581
|
}
|
|
552
582
|
|
|
553
583
|
export function secretRouteDefinitions(
|
|
@@ -557,7 +587,7 @@ export function secretRouteDefinitions(
|
|
|
557
587
|
{
|
|
558
588
|
endpoint: "secrets",
|
|
559
589
|
method: "POST",
|
|
560
|
-
handler: async ({ req }) => handleAddSecret(req, deps
|
|
590
|
+
handler: async ({ req }) => handleAddSecret(req, deps),
|
|
561
591
|
summary: "Add a secret",
|
|
562
592
|
description:
|
|
563
593
|
"Store a new secret (API key, OAuth token, etc.) in the credential vault.",
|
|
@@ -576,7 +606,7 @@ export function secretRouteDefinitions(
|
|
|
576
606
|
{
|
|
577
607
|
endpoint: "secrets",
|
|
578
608
|
method: "DELETE",
|
|
579
|
-
handler: async ({ req }) => handleDeleteSecret(req),
|
|
609
|
+
handler: async ({ req }) => handleDeleteSecret(req, deps),
|
|
580
610
|
summary: "Delete a secret",
|
|
581
611
|
description: "Remove a secret from the credential vault by name.",
|
|
582
612
|
tags: ["secrets"],
|
|
@@ -30,10 +30,6 @@ import {
|
|
|
30
30
|
getMostRecentAppByProvider,
|
|
31
31
|
getProvider,
|
|
32
32
|
} from "../../oauth/oauth-store.js";
|
|
33
|
-
import {
|
|
34
|
-
getProviderBehavior,
|
|
35
|
-
resolveService,
|
|
36
|
-
} from "../../oauth/provider-behaviors.js";
|
|
37
33
|
import {
|
|
38
34
|
check,
|
|
39
35
|
classifyRisk,
|
|
@@ -170,14 +166,14 @@ async function handleOAuthConnectStart(body: {
|
|
|
170
166
|
return httpError("BAD_REQUEST", "Missing required field: service", 400);
|
|
171
167
|
}
|
|
172
168
|
|
|
173
|
-
const
|
|
169
|
+
const service = body.service;
|
|
174
170
|
|
|
175
171
|
// Resolve client_id and client_secret from oauth-store.
|
|
176
172
|
let clientId: string | undefined;
|
|
177
173
|
let clientSecret: string | undefined;
|
|
178
174
|
|
|
179
175
|
// Try existing connection first (re-auth flow)
|
|
180
|
-
const conn = getConnectionByProvider(
|
|
176
|
+
const conn = getConnectionByProvider(service);
|
|
181
177
|
if (conn) {
|
|
182
178
|
const app = getApp(conn.oauthAppId);
|
|
183
179
|
if (app) {
|
|
@@ -188,7 +184,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
188
184
|
|
|
189
185
|
// Fall back to most recent app for this provider (first-time connect with stored app)
|
|
190
186
|
if (!clientId) {
|
|
191
|
-
const dbApp = getMostRecentAppByProvider(
|
|
187
|
+
const dbApp = getMostRecentAppByProvider(service);
|
|
192
188
|
if (dbApp) {
|
|
193
189
|
clientId = dbApp.clientId;
|
|
194
190
|
if (!clientSecret) {
|
|
@@ -202,20 +198,17 @@ async function handleOAuthConnectStart(body: {
|
|
|
202
198
|
if (!clientId) {
|
|
203
199
|
return httpError(
|
|
204
200
|
"BAD_REQUEST",
|
|
205
|
-
`No client_id found for "${
|
|
201
|
+
`No client_id found for "${service}". Store it first via the credential vault.`,
|
|
206
202
|
400,
|
|
207
203
|
);
|
|
208
204
|
}
|
|
209
205
|
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const requiresSecret =
|
|
213
|
-
behavior?.setup?.requiresClientSecret ??
|
|
214
|
-
!!(providerRow?.tokenEndpointAuthMethod || providerRow?.extraParams);
|
|
206
|
+
const providerRow = getProvider(service);
|
|
207
|
+
const requiresSecret = !!providerRow?.requiresClientSecret;
|
|
215
208
|
if (requiresSecret && !clientSecret) {
|
|
216
209
|
return httpError(
|
|
217
210
|
"BAD_REQUEST",
|
|
218
|
-
`client_secret is required for "${
|
|
211
|
+
`client_secret is required for "${service}" but not found in the credential store. Store it first via the credential vault.`,
|
|
219
212
|
400,
|
|
220
213
|
);
|
|
221
214
|
}
|
|
@@ -226,7 +219,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
226
219
|
let authUrl: string | undefined;
|
|
227
220
|
|
|
228
221
|
const result = await orchestrateOAuthConnect({
|
|
229
|
-
service
|
|
222
|
+
service,
|
|
230
223
|
requestedScopes: body.requestedScopes,
|
|
231
224
|
clientId,
|
|
232
225
|
clientSecret,
|
|
@@ -238,7 +231,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
238
231
|
// Prefer accountInfo from oauth-store when available.
|
|
239
232
|
let accountInfo = deferredResult.accountInfo;
|
|
240
233
|
try {
|
|
241
|
-
const conn = getConnectionByProvider(
|
|
234
|
+
const conn = getConnectionByProvider(service);
|
|
242
235
|
if (conn?.accountInfo) accountInfo = conn.accountInfo;
|
|
243
236
|
} catch {
|
|
244
237
|
// DB not ready — use orchestrator value
|
|
@@ -277,7 +270,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
277
270
|
|
|
278
271
|
if (!result.success) {
|
|
279
272
|
log.error(
|
|
280
|
-
{ err: result.error, service
|
|
273
|
+
{ err: result.error, service },
|
|
281
274
|
"OAuth connect orchestrator returned error",
|
|
282
275
|
);
|
|
283
276
|
return httpError(
|
|
@@ -298,7 +291,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
298
291
|
// Prefer accountInfo from oauth-store when available.
|
|
299
292
|
let responseAccountInfo = result.accountInfo;
|
|
300
293
|
try {
|
|
301
|
-
const conn = getConnectionByProvider(
|
|
294
|
+
const conn = getConnectionByProvider(service);
|
|
302
295
|
if (conn?.accountInfo) responseAccountInfo = conn.accountInfo;
|
|
303
296
|
} catch {
|
|
304
297
|
// DB not ready — use orchestrator value
|
|
@@ -312,7 +305,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
312
305
|
});
|
|
313
306
|
} catch (err) {
|
|
314
307
|
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
-
log.error({ err, service
|
|
308
|
+
log.error({ err, service }, "OAuth connect flow failed");
|
|
316
309
|
return httpError("INTERNAL_ERROR", sanitizeOAuthError(message), 500);
|
|
317
310
|
}
|
|
318
311
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
inspectSkill,
|
|
24
24
|
installSkill,
|
|
25
25
|
listSkills,
|
|
26
|
+
listSkillsWithCatalog,
|
|
26
27
|
searchSkills,
|
|
27
28
|
uninstallSkill,
|
|
28
29
|
updateSkill,
|
|
@@ -47,13 +48,53 @@ export function skillRouteDefinitions(deps: SkillRouteDeps): RouteDefinition[] {
|
|
|
47
48
|
method: "GET",
|
|
48
49
|
policyKey: "skills",
|
|
49
50
|
summary: "List all skills",
|
|
50
|
-
description:
|
|
51
|
+
description:
|
|
52
|
+
"Return all installed skills. Pass ?include=catalog to also include available catalog skills.",
|
|
51
53
|
tags: ["skills"],
|
|
54
|
+
queryParams: [
|
|
55
|
+
{
|
|
56
|
+
name: "include",
|
|
57
|
+
schema: { type: "string", enum: ["catalog"] },
|
|
58
|
+
description:
|
|
59
|
+
"Optional inclusion flag. Use 'catalog' to merge available Vellum catalog skills into the response.",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
52
62
|
responseBody: z.object({
|
|
53
|
-
skills: z
|
|
63
|
+
skills: z
|
|
64
|
+
.array(
|
|
65
|
+
z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
name: z.string(),
|
|
68
|
+
description: z.string(),
|
|
69
|
+
emoji: z.string().optional(),
|
|
70
|
+
homepage: z.string().optional(),
|
|
71
|
+
source: z.enum([
|
|
72
|
+
"bundled",
|
|
73
|
+
"managed",
|
|
74
|
+
"workspace",
|
|
75
|
+
"clawhub",
|
|
76
|
+
"extra",
|
|
77
|
+
"catalog",
|
|
78
|
+
]),
|
|
79
|
+
state: z.enum(["enabled", "disabled"]),
|
|
80
|
+
installStatus: z.enum(["bundled", "installed", "available"]),
|
|
81
|
+
updateAvailable: z.boolean(),
|
|
82
|
+
provenance: z.object({
|
|
83
|
+
kind: z.enum(["first-party", "third-party", "local"]),
|
|
84
|
+
provider: z.string().optional(),
|
|
85
|
+
originId: z.string().optional(),
|
|
86
|
+
sourceUrl: z.string().optional(),
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
.describe("Skill objects"),
|
|
54
91
|
}),
|
|
55
|
-
handler: () => {
|
|
56
|
-
const
|
|
92
|
+
handler: async ({ url }) => {
|
|
93
|
+
const include = url.searchParams.get("include");
|
|
94
|
+
const skills =
|
|
95
|
+
include === "catalog"
|
|
96
|
+
? await listSkillsWithCatalog(ctx())
|
|
97
|
+
: listSkills(ctx());
|
|
57
98
|
return Response.json({ skills });
|
|
58
99
|
},
|
|
59
100
|
},
|
|
@@ -15,12 +15,12 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
15
15
|
{
|
|
16
16
|
name: "Gmail",
|
|
17
17
|
category: "email",
|
|
18
|
-
isConnected: () => isProviderConnected("
|
|
18
|
+
isConnected: () => isProviderConnected("google"),
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
name: "Slack",
|
|
22
22
|
category: "messaging",
|
|
23
|
-
isConnected: () => isProviderConnected("
|
|
23
|
+
isConnected: () => isProviderConnected("slack"),
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: "Twilio",
|
|
@@ -30,11 +30,13 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async get(account: string): Promise<CredentialGetResult> {
|
|
33
|
+
if (!this.isAvailable()) {
|
|
34
|
+
return { value: undefined, unreachable: true };
|
|
35
|
+
}
|
|
33
36
|
try {
|
|
34
|
-
const result = await this.client.call(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
37
|
+
const result = await this.client.call(CesRpcMethod.GetCredential, {
|
|
38
|
+
account,
|
|
39
|
+
});
|
|
38
40
|
return {
|
|
39
41
|
value: result.found ? result.value : undefined,
|
|
40
42
|
unreachable: false,
|
|
@@ -46,11 +48,12 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
async set(account: string, value: string): Promise<boolean> {
|
|
51
|
+
if (!this.isAvailable()) return false;
|
|
49
52
|
try {
|
|
50
|
-
const result = await this.client.call(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
53
|
+
const result = await this.client.call(CesRpcMethod.SetCredential, {
|
|
54
|
+
account,
|
|
55
|
+
value,
|
|
56
|
+
});
|
|
54
57
|
return result.ok;
|
|
55
58
|
} catch (err) {
|
|
56
59
|
log.warn({ err, account }, "CES RPC credential set failed");
|
|
@@ -59,11 +62,11 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
async delete(account: string): Promise<DeleteResult> {
|
|
65
|
+
if (!this.isAvailable()) return "error";
|
|
62
66
|
try {
|
|
63
|
-
const result = await this.client.call(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
+
const result = await this.client.call(CesRpcMethod.DeleteCredential, {
|
|
68
|
+
account,
|
|
69
|
+
});
|
|
67
70
|
return result.result;
|
|
68
71
|
} catch (err) {
|
|
69
72
|
log.warn({ err, account }, "CES RPC credential delete failed");
|
|
@@ -72,11 +75,11 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async list(): Promise<CredentialListResult> {
|
|
78
|
+
if (!this.isAvailable()) {
|
|
79
|
+
return { accounts: [], unreachable: true };
|
|
80
|
+
}
|
|
75
81
|
try {
|
|
76
|
-
const result = await this.client.call(
|
|
77
|
-
CesRpcMethod.ListCredentials,
|
|
78
|
-
{},
|
|
79
|
-
);
|
|
82
|
+
const result = await this.client.call(CesRpcMethod.ListCredentials, {});
|
|
80
83
|
return { accounts: result.accounts, unreachable: false };
|
|
81
84
|
} catch (err) {
|
|
82
85
|
log.warn({ err }, "CES RPC credential list failed");
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML rendering for OAuth completion pages shown in the browser
|
|
3
|
+
* after a loopback/redirect OAuth flow completes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function escapeHtml(s: string): string {
|
|
7
|
+
return s
|
|
8
|
+
.replace(/&/g, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatProviderName(provider: string): string {
|
|
16
|
+
// Capitalize first letter of each word, handle common acronyms
|
|
17
|
+
return provider
|
|
18
|
+
.split(/[-_]/)
|
|
19
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
20
|
+
.join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderOAuthCompletionPage(
|
|
24
|
+
message: string,
|
|
25
|
+
success: boolean,
|
|
26
|
+
provider?: string,
|
|
27
|
+
): string {
|
|
28
|
+
const displayProvider = provider ? formatProviderName(provider) : "";
|
|
29
|
+
const title = success
|
|
30
|
+
? displayProvider
|
|
31
|
+
? `Connected to ${escapeHtml(displayProvider)}`
|
|
32
|
+
: "Authorization Successful"
|
|
33
|
+
: "Authorization Failed";
|
|
34
|
+
const subtitle = success
|
|
35
|
+
? "You can close this tab and return to your assistant."
|
|
36
|
+
: escapeHtml(message);
|
|
37
|
+
|
|
38
|
+
const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
39
|
+
<circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
|
|
40
|
+
<path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
41
|
+
</svg>`;
|
|
42
|
+
|
|
43
|
+
const errorSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
44
|
+
<circle cx="28" cy="28" r="28" fill="var(--negative-bg)"/>
|
|
45
|
+
<path class="cross cross-1" d="M20 20L36 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
46
|
+
<path class="cross cross-2" d="M36 20L20 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
47
|
+
</svg>`;
|
|
48
|
+
|
|
49
|
+
return `<!DOCTYPE html>
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
54
|
+
<title>${escapeHtml(title)}</title>
|
|
55
|
+
<style>
|
|
56
|
+
:root {
|
|
57
|
+
--surface: #F5F3EB;
|
|
58
|
+
--surface-card: #FFFFFF;
|
|
59
|
+
--card-border: #E8E6DA;
|
|
60
|
+
--text-primary: #2A2A28;
|
|
61
|
+
--text-secondary: #4A4A46;
|
|
62
|
+
--text-tertiary: #A1A096;
|
|
63
|
+
--positive-bg: #D4DFD0;
|
|
64
|
+
--positive-fg: #516748;
|
|
65
|
+
--negative-bg: #F7DAC9;
|
|
66
|
+
--negative-fg: #DA491A;
|
|
67
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
|
|
68
|
+
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
69
|
+
}
|
|
70
|
+
@media (prefers-color-scheme: dark) {
|
|
71
|
+
:root {
|
|
72
|
+
--surface: #1A1A18;
|
|
73
|
+
--surface-card: #2A2A28;
|
|
74
|
+
--card-border: #3A3A37;
|
|
75
|
+
--text-primary: #F5F3EB;
|
|
76
|
+
--text-secondary: #BDB9A9;
|
|
77
|
+
--text-tertiary: #6B6B65;
|
|
78
|
+
--positive-bg: #1A2316;
|
|
79
|
+
--positive-fg: #7A8B6F;
|
|
80
|
+
--negative-bg: #4E281D;
|
|
81
|
+
--negative-fg: #E86B40;
|
|
82
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
86
|
+
body {
|
|
87
|
+
font-family: var(--font);
|
|
88
|
+
background: var(--surface);
|
|
89
|
+
color: var(--text-primary);
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
min-height: 100vh;
|
|
94
|
+
-webkit-font-smoothing: antialiased;
|
|
95
|
+
}
|
|
96
|
+
.card {
|
|
97
|
+
text-align: center;
|
|
98
|
+
padding: 48px 40px 40px;
|
|
99
|
+
background: var(--surface-card);
|
|
100
|
+
border: 1px solid var(--card-border);
|
|
101
|
+
border-radius: 16px;
|
|
102
|
+
box-shadow: var(--shadow);
|
|
103
|
+
max-width: 380px;
|
|
104
|
+
width: 100%;
|
|
105
|
+
opacity: 0;
|
|
106
|
+
transform: translateY(8px) scale(0.98);
|
|
107
|
+
animation: cardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
|
|
108
|
+
}
|
|
109
|
+
@keyframes cardIn {
|
|
110
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
111
|
+
}
|
|
112
|
+
.icon {
|
|
113
|
+
width: 56px;
|
|
114
|
+
height: 56px;
|
|
115
|
+
margin-bottom: 20px;
|
|
116
|
+
}
|
|
117
|
+
.check {
|
|
118
|
+
stroke-dasharray: 32;
|
|
119
|
+
stroke-dashoffset: 32;
|
|
120
|
+
animation: draw 0.4s ease-out 0.45s forwards;
|
|
121
|
+
}
|
|
122
|
+
.cross {
|
|
123
|
+
stroke-dasharray: 22;
|
|
124
|
+
stroke-dashoffset: 22;
|
|
125
|
+
}
|
|
126
|
+
.cross-1 { animation: draw 0.3s ease-out 0.45s forwards; }
|
|
127
|
+
.cross-2 { animation: draw 0.3s ease-out 0.55s forwards; }
|
|
128
|
+
@keyframes draw {
|
|
129
|
+
to { stroke-dashoffset: 0; }
|
|
130
|
+
}
|
|
131
|
+
h1 {
|
|
132
|
+
font-size: 18px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
letter-spacing: -0.2px;
|
|
135
|
+
color: var(--text-primary);
|
|
136
|
+
margin-bottom: 6px;
|
|
137
|
+
}
|
|
138
|
+
p {
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
line-height: 1.5;
|
|
141
|
+
color: var(--text-secondary);
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
<div class="card">
|
|
147
|
+
${success ? checkmarkSvg : errorSvg}
|
|
148
|
+
<h1>${escapeHtml(title)}</h1>
|
|
149
|
+
<p>${subtitle}</p>
|
|
150
|
+
</div>
|
|
151
|
+
</body>
|
|
152
|
+
</html>`;
|
|
153
|
+
}
|