@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route definitions for message text-to-speech synthesis.
|
|
3
|
+
*
|
|
4
|
+
* POST /v1/messages/:id/tts?conversationId=... — synthesize message text to audio
|
|
5
|
+
*
|
|
6
|
+
* Gated behind the `feature_flags.message-tts.enabled` assistant feature flag.
|
|
7
|
+
* Uses Fish Audio for synthesis when configured.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { synthesizeWithFishAudio } from "../../calls/fish-audio-client.js";
|
|
11
|
+
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
12
|
+
import { getConfig } from "../../config/loader.js";
|
|
13
|
+
import { getMessageContent } from "../../daemon/handlers/conversation-history.js";
|
|
14
|
+
import { getLogger } from "../../util/logger.js";
|
|
15
|
+
import { httpError } from "../http-errors.js";
|
|
16
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
17
|
+
|
|
18
|
+
const log = getLogger("tts-routes");
|
|
19
|
+
|
|
20
|
+
const MESSAGE_TTS_FLAG = "feature_flags.message-tts.enabled" as const;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Route definitions
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export function ttsRouteDefinitions(): RouteDefinition[] {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
endpoint: "messages/:id/tts",
|
|
30
|
+
method: "POST",
|
|
31
|
+
policyKey: "messages/tts",
|
|
32
|
+
handler: async ({ url, params }) => {
|
|
33
|
+
const config = getConfig();
|
|
34
|
+
|
|
35
|
+
if (!isAssistantFeatureFlagEnabled(MESSAGE_TTS_FLAG, config)) {
|
|
36
|
+
return httpError("FORBIDDEN", "Message TTS is not enabled", 403);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const messageId = params.id;
|
|
40
|
+
const conversationId =
|
|
41
|
+
url.searchParams.get("conversationId") ?? undefined;
|
|
42
|
+
|
|
43
|
+
const result = getMessageContent(messageId, conversationId);
|
|
44
|
+
if (!result) {
|
|
45
|
+
return httpError("NOT_FOUND", `Message ${messageId} not found`, 404);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!result.text) {
|
|
49
|
+
return httpError("BAD_REQUEST", "Message has no text content", 400);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { fishAudio } = config;
|
|
53
|
+
if (!fishAudio?.referenceId) {
|
|
54
|
+
return httpError(
|
|
55
|
+
"SERVICE_UNAVAILABLE",
|
|
56
|
+
"Fish Audio TTS is not configured",
|
|
57
|
+
503,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const audioBuffer = await synthesizeWithFishAudio(
|
|
63
|
+
result.text,
|
|
64
|
+
fishAudio,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const format = fishAudio.format ?? "mp3";
|
|
68
|
+
const contentType =
|
|
69
|
+
format === "wav"
|
|
70
|
+
? "audio/wav"
|
|
71
|
+
: format === "opus"
|
|
72
|
+
? "audio/opus"
|
|
73
|
+
: "audio/mpeg";
|
|
74
|
+
|
|
75
|
+
return new Response(new Uint8Array(audioBuffer), {
|
|
76
|
+
status: 200,
|
|
77
|
+
headers: { "Content-Type": contentType },
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
log.error({ err, messageId }, "TTS synthesis failed");
|
|
81
|
+
return httpError("INTERNAL_ERROR", "TTS synthesis failed", 502);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Upgrade broadcast endpoint — publishes service group update lifecycle
|
|
3
|
-
* events (starting / complete) to all connected SSE clients.
|
|
3
|
+
* events (starting / progress / complete) to all connected SSE clients.
|
|
4
4
|
*
|
|
5
5
|
* Protected by a route policy restricting access to gateway service
|
|
6
6
|
* principals only (`svc_gateway` with `internal.write` scope), following
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
ServiceGroupUpdateComplete,
|
|
14
|
+
ServiceGroupUpdateProgress,
|
|
14
15
|
ServiceGroupUpdateStarting,
|
|
15
16
|
} from "../../daemon/message-types/upgrades.js";
|
|
16
17
|
import { buildAssistantEvent } from "../assistant-event.js";
|
|
@@ -86,6 +87,29 @@ export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
|
|
|
86
87
|
return Response.json({ ok: true });
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
if (type === "progress") {
|
|
91
|
+
const { statusMessage } = body as { statusMessage?: unknown };
|
|
92
|
+
|
|
93
|
+
if (typeof statusMessage !== "string" || statusMessage.length === 0) {
|
|
94
|
+
return httpError(
|
|
95
|
+
"BAD_REQUEST",
|
|
96
|
+
"statusMessage is required and must be a non-empty string",
|
|
97
|
+
400,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const message: ServiceGroupUpdateProgress = {
|
|
102
|
+
type: "service_group_update_progress",
|
|
103
|
+
statusMessage,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await assistantEventHub.publish(
|
|
107
|
+
buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, message),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return Response.json({ ok: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
89
113
|
if (type === "complete") {
|
|
90
114
|
const { installedVersion, success, rolledBackToVersion } = body as {
|
|
91
115
|
installedVersion?: unknown;
|
|
@@ -142,7 +166,7 @@ export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
|
|
|
142
166
|
|
|
143
167
|
return httpError(
|
|
144
168
|
"BAD_REQUEST",
|
|
145
|
-
'type must be "starting" or "complete"',
|
|
169
|
+
'type must be "starting", "progress", or "complete"',
|
|
146
170
|
400,
|
|
147
171
|
);
|
|
148
172
|
},
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace commit endpoint — creates a git commit in the workspace
|
|
3
|
+
* directory with all pending changes.
|
|
4
|
+
*
|
|
5
|
+
* Protected by a route policy restricting access to gateway service
|
|
6
|
+
* principals only (`svc_gateway` with `internal.write` scope), following
|
|
7
|
+
* the same pattern as other gateway-forwarded control-plane endpoints.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getWorkspaceDir } from "../../util/platform.js";
|
|
11
|
+
import { getWorkspaceGitService } from "../../workspace/git-service.js";
|
|
12
|
+
import { httpError } from "../http-errors.js";
|
|
13
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
14
|
+
|
|
15
|
+
export function workspaceCommitRouteDefinitions(): RouteDefinition[] {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
endpoint: "admin/workspace-commit",
|
|
19
|
+
method: "POST",
|
|
20
|
+
handler: async ({ req }) => {
|
|
21
|
+
let body: unknown;
|
|
22
|
+
try {
|
|
23
|
+
body = await req.json();
|
|
24
|
+
} catch {
|
|
25
|
+
return httpError("BAD_REQUEST", "Invalid JSON body", 400);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!body || typeof body !== "object") {
|
|
29
|
+
return httpError(
|
|
30
|
+
"BAD_REQUEST",
|
|
31
|
+
"Request body must be a JSON object",
|
|
32
|
+
400,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { message } = body as { message?: unknown };
|
|
37
|
+
|
|
38
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
39
|
+
return httpError(
|
|
40
|
+
"BAD_REQUEST",
|
|
41
|
+
"message is required and must be a non-empty string",
|
|
42
|
+
400,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await getWorkspaceGitService(getWorkspaceDir()).commitChanges(
|
|
48
|
+
message,
|
|
49
|
+
);
|
|
50
|
+
return Response.json({ ok: true });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const detail = err instanceof Error ? err.message : "Unknown error";
|
|
53
|
+
return httpError(
|
|
54
|
+
"INTERNAL_ERROR",
|
|
55
|
+
`Workspace commit failed: ${detail}`,
|
|
56
|
+
500,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
@@ -190,9 +190,30 @@ describe("isTextMimeType", () => {
|
|
|
190
190
|
expect(isTextMimeType("video/mp4")).toBe(false);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
test("application/octet-stream is not text", () => {
|
|
193
|
+
test("application/octet-stream is not text without filename", () => {
|
|
194
194
|
expect(isTextMimeType("application/octet-stream")).toBe(false);
|
|
195
195
|
});
|
|
196
|
+
|
|
197
|
+
test("application/octet-stream with .py filename is text", () => {
|
|
198
|
+
expect(isTextMimeType("application/octet-stream", "script.py")).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("application/octet-stream with .go filename is text", () => {
|
|
202
|
+
expect(isTextMimeType("application/octet-stream", "main.go")).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("application/octet-stream with .rs filename is text", () => {
|
|
206
|
+
expect(isTextMimeType("application/octet-stream", "lib.rs")).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("application/octet-stream with unknown extension is not text", () => {
|
|
210
|
+
expect(isTextMimeType("application/octet-stream", "data.bin")).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("extension fallback only applies to application/octet-stream", () => {
|
|
214
|
+
// A binary plist has a specific MIME type — extension should not override it
|
|
215
|
+
expect(isTextMimeType("application/x-plist", "Info.plist")).toBe(false);
|
|
216
|
+
});
|
|
196
217
|
});
|
|
197
218
|
|
|
198
219
|
// ===========================================================================
|
|
@@ -119,7 +119,7 @@ function handleWorkspaceFile(ctx: RouteContext): Response {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const mimeType = Bun.file(resolved).type;
|
|
122
|
-
const isText = isTextMimeType(mimeType);
|
|
122
|
+
const isText = isTextMimeType(mimeType, basename(resolved));
|
|
123
123
|
const isBinary = !isText;
|
|
124
124
|
|
|
125
125
|
let content: string | undefined = undefined;
|
|
@@ -85,10 +85,94 @@ const TEXT_MIME_PREFIXES = [
|
|
|
85
85
|
"application/x-yaml",
|
|
86
86
|
"application/toml",
|
|
87
87
|
"application/x-sh",
|
|
88
|
+
"application/x-httpd-php",
|
|
89
|
+
"application/x-perl",
|
|
90
|
+
"application/x-sql",
|
|
91
|
+
"application/x-tex",
|
|
92
|
+
"application/vnd.dart",
|
|
88
93
|
];
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
/**
|
|
96
|
+
* File extensions that are known text/code files but that Bun's MIME
|
|
97
|
+
* detection reports as `application/octet-stream`.
|
|
98
|
+
*/
|
|
99
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
100
|
+
// Programming languages
|
|
101
|
+
"py",
|
|
102
|
+
"rb",
|
|
103
|
+
"go",
|
|
104
|
+
"rs",
|
|
105
|
+
"swift",
|
|
106
|
+
"kt",
|
|
107
|
+
"kts",
|
|
108
|
+
"cs",
|
|
109
|
+
"scala",
|
|
110
|
+
"ex",
|
|
111
|
+
"exs",
|
|
112
|
+
"erl",
|
|
113
|
+
"hs",
|
|
114
|
+
"clj",
|
|
115
|
+
"cljs",
|
|
116
|
+
"jl",
|
|
117
|
+
"zig",
|
|
118
|
+
"nim",
|
|
119
|
+
"v",
|
|
120
|
+
"sol",
|
|
121
|
+
"r",
|
|
122
|
+
"java",
|
|
123
|
+
"lua",
|
|
124
|
+
// Shell / scripting
|
|
125
|
+
"bash",
|
|
126
|
+
"zsh",
|
|
127
|
+
"fish",
|
|
128
|
+
"ps1",
|
|
129
|
+
"bat",
|
|
130
|
+
"cmd",
|
|
131
|
+
"awk",
|
|
132
|
+
// Web frameworks
|
|
133
|
+
"vue",
|
|
134
|
+
"svelte",
|
|
135
|
+
"scss",
|
|
136
|
+
"sass",
|
|
137
|
+
"less",
|
|
138
|
+
// Config / data
|
|
139
|
+
"cfg",
|
|
140
|
+
"conf",
|
|
141
|
+
"ini",
|
|
142
|
+
"properties",
|
|
143
|
+
"env",
|
|
144
|
+
"gradle",
|
|
145
|
+
"cmake",
|
|
146
|
+
// Markup / docs
|
|
147
|
+
"rst",
|
|
148
|
+
"adoc",
|
|
149
|
+
"org",
|
|
150
|
+
"tex",
|
|
151
|
+
"latex",
|
|
152
|
+
// Other text formats
|
|
153
|
+
"graphql",
|
|
154
|
+
"gql",
|
|
155
|
+
"proto",
|
|
156
|
+
"tf",
|
|
157
|
+
"hcl",
|
|
158
|
+
"diff",
|
|
159
|
+
"patch",
|
|
160
|
+
"log",
|
|
161
|
+
"lock",
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
export function isTextMimeType(mimeType: string, fileName?: string): boolean {
|
|
165
|
+
if (TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix))) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Only fall back to extension check when the MIME type is genuinely unknown.
|
|
169
|
+
// Specific MIME types (e.g. application/x-plist for binary plists) should be
|
|
170
|
+
// trusted over the extension — overriding them risks corrupting binary files.
|
|
171
|
+
if (fileName && mimeType === "application/octet-stream") {
|
|
172
|
+
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
173
|
+
if (ext && TEXT_FILE_EXTENSIONS.has(ext)) return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
92
176
|
}
|
|
93
177
|
|
|
94
178
|
export const MAX_INLINE_TEXT_SIZE = 2 * 1024 * 1024; // 2 MB
|
|
@@ -16,11 +16,17 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { getLogger } from "../util/logger.js";
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
CredentialBackend,
|
|
21
|
+
CredentialGetResult,
|
|
22
|
+
DeleteResult,
|
|
23
|
+
} from "./credential-backend.js";
|
|
20
24
|
|
|
21
25
|
const log = getLogger("ces-credential-client");
|
|
22
26
|
|
|
23
27
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
28
|
+
const SET_MAX_RETRIES = 3;
|
|
29
|
+
const SET_RETRY_DELAY_MS = 2_000;
|
|
24
30
|
|
|
25
31
|
// ---------------------------------------------------------------------------
|
|
26
32
|
// Env helpers
|
|
@@ -77,49 +83,80 @@ export class CesCredentialBackend implements CredentialBackend {
|
|
|
77
83
|
return !!getBaseUrl() && !!getServiceToken();
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
async get(account: string): Promise<
|
|
86
|
+
async get(account: string): Promise<CredentialGetResult> {
|
|
81
87
|
try {
|
|
82
88
|
const res = await cesRequest(
|
|
83
89
|
"GET",
|
|
84
90
|
`/v1/credentials/${encodeURIComponent(account)}`,
|
|
85
91
|
);
|
|
86
|
-
if (!res) return undefined;
|
|
87
|
-
if (res.status === 404) return undefined;
|
|
92
|
+
if (!res) return { value: undefined, unreachable: true };
|
|
93
|
+
if (res.status === 404) return { value: undefined, unreachable: false };
|
|
88
94
|
if (!res.ok) {
|
|
89
95
|
log.warn(
|
|
90
96
|
{ account, status: res.status },
|
|
91
97
|
"CES credential get returned non-OK status",
|
|
92
98
|
);
|
|
93
|
-
return undefined;
|
|
99
|
+
return { value: undefined, unreachable: true };
|
|
94
100
|
}
|
|
95
101
|
const data = (await res.json()) as { value?: string };
|
|
96
|
-
return data.value;
|
|
102
|
+
return { value: data.value, unreachable: false };
|
|
97
103
|
} catch (err) {
|
|
98
104
|
log.warn({ err, account }, "CES credential get threw unexpectedly");
|
|
99
|
-
return undefined;
|
|
105
|
+
return { value: undefined, unreachable: true };
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
async set(account: string, value: string): Promise<boolean> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!res) return false;
|
|
111
|
-
if (!res.ok) {
|
|
112
|
-
log.warn(
|
|
113
|
-
{ account, status: res.status },
|
|
114
|
-
"CES credential set returned non-OK status",
|
|
110
|
+
for (let attempt = 0; attempt < SET_MAX_RETRIES; attempt++) {
|
|
111
|
+
try {
|
|
112
|
+
const res = await cesRequest(
|
|
113
|
+
"POST",
|
|
114
|
+
`/v1/credentials/${encodeURIComponent(account)}`,
|
|
115
|
+
{ value },
|
|
115
116
|
);
|
|
117
|
+
if (!res) {
|
|
118
|
+
// CES not reachable or env vars missing — retry in case sidecar
|
|
119
|
+
// is still starting up after pod creation.
|
|
120
|
+
if (attempt < SET_MAX_RETRIES - 1) {
|
|
121
|
+
log.warn(
|
|
122
|
+
{ account, attempt },
|
|
123
|
+
"CES credential set got no response, retrying",
|
|
124
|
+
);
|
|
125
|
+
await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
if (attempt < SET_MAX_RETRIES - 1) {
|
|
132
|
+
log.warn(
|
|
133
|
+
{ account, status: res.status, attempt },
|
|
134
|
+
"CES credential set returned non-OK status, retrying",
|
|
135
|
+
);
|
|
136
|
+
await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
log.warn(
|
|
140
|
+
{ account, status: res.status },
|
|
141
|
+
"CES credential set returned non-OK status",
|
|
142
|
+
);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (attempt < SET_MAX_RETRIES - 1) {
|
|
148
|
+
log.warn(
|
|
149
|
+
{ err, account, attempt },
|
|
150
|
+
"CES credential set threw, retrying",
|
|
151
|
+
);
|
|
152
|
+
await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
log.warn({ err, account }, "CES credential set threw unexpectedly");
|
|
116
156
|
return false;
|
|
117
157
|
}
|
|
118
|
-
return true;
|
|
119
|
-
} catch (err) {
|
|
120
|
-
log.warn({ err, account }, "CES credential set threw unexpectedly");
|
|
121
|
-
return false;
|
|
122
158
|
}
|
|
159
|
+
return false;
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
async delete(account: string): Promise<DeleteResult> {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CesRpcCredentialBackend — a CredentialBackend that delegates all credential
|
|
3
|
+
* operations to the Credential Execution Service (CES) via stdio RPC.
|
|
4
|
+
*
|
|
5
|
+
* Maps RPC responses to the existing CredentialGetResult and DeleteResult
|
|
6
|
+
* types. Errors are caught and mapped to unreachable/error states for
|
|
7
|
+
* graceful fallback.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CesRpcMethod } from "@vellumai/ces-contracts";
|
|
11
|
+
|
|
12
|
+
import type { CesClient } from "../credential-execution/client.js";
|
|
13
|
+
import { getLogger } from "../util/logger.js";
|
|
14
|
+
import type {
|
|
15
|
+
CredentialBackend,
|
|
16
|
+
CredentialGetResult,
|
|
17
|
+
DeleteResult,
|
|
18
|
+
} from "./credential-backend.js";
|
|
19
|
+
|
|
20
|
+
const log = getLogger("ces-rpc-credential-backend");
|
|
21
|
+
|
|
22
|
+
export class CesRpcCredentialBackend implements CredentialBackend {
|
|
23
|
+
readonly name = "ces-rpc";
|
|
24
|
+
|
|
25
|
+
constructor(private readonly client: CesClient) {}
|
|
26
|
+
|
|
27
|
+
isAvailable(): boolean {
|
|
28
|
+
return this.client.isReady();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async get(account: string): Promise<CredentialGetResult> {
|
|
32
|
+
try {
|
|
33
|
+
const result = await this.client.call(
|
|
34
|
+
CesRpcMethod.GetCredential,
|
|
35
|
+
{ account },
|
|
36
|
+
);
|
|
37
|
+
return {
|
|
38
|
+
value: result.found ? result.value : undefined,
|
|
39
|
+
unreachable: false,
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.warn({ err, account }, "CES RPC credential get failed");
|
|
43
|
+
return { value: undefined, unreachable: true };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async set(account: string, value: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const result = await this.client.call(
|
|
50
|
+
CesRpcMethod.SetCredential,
|
|
51
|
+
{ account, value },
|
|
52
|
+
);
|
|
53
|
+
return result.ok;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
log.warn({ err, account }, "CES RPC credential set failed");
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async delete(account: string): Promise<DeleteResult> {
|
|
61
|
+
try {
|
|
62
|
+
const result = await this.client.call(
|
|
63
|
+
CesRpcMethod.DeleteCredential,
|
|
64
|
+
{ account },
|
|
65
|
+
);
|
|
66
|
+
return result.result;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
log.warn({ err, account }, "CES RPC credential delete failed");
|
|
69
|
+
return "error";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async list(): Promise<string[]> {
|
|
74
|
+
try {
|
|
75
|
+
const result = await this.client.call(
|
|
76
|
+
CesRpcMethod.ListCredentials,
|
|
77
|
+
{},
|
|
78
|
+
);
|
|
79
|
+
return result.accounts;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log.warn({ err }, "CES RPC credential list failed");
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CredentialBackend interface and adapters — abstracts credential storage
|
|
3
3
|
* behind a unified async API so callers don't need to know which backend
|
|
4
|
-
*
|
|
4
|
+
* is in use.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { getLogger } from "../util/logger.js";
|
|
8
7
|
import * as encryptedStore from "./encrypted-store.js";
|
|
9
|
-
import type { KeychainBrokerClient } from "./keychain-broker-client.js";
|
|
10
|
-
import { createBrokerClient } from "./keychain-broker-client.js";
|
|
11
|
-
|
|
12
|
-
const log = getLogger("credential-backend");
|
|
13
8
|
|
|
14
9
|
// ---------------------------------------------------------------------------
|
|
15
10
|
// Types
|
|
@@ -18,6 +13,12 @@ const log = getLogger("credential-backend");
|
|
|
18
13
|
/** Result of a delete operation — distinguishes success, not-found, and error. */
|
|
19
14
|
export type DeleteResult = "deleted" | "not-found" | "error";
|
|
20
15
|
|
|
16
|
+
/** Result of a get operation — distinguishes unreachable from not-found. */
|
|
17
|
+
export interface CredentialGetResult {
|
|
18
|
+
value: string | undefined;
|
|
19
|
+
unreachable: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Interface
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -29,8 +30,8 @@ export interface CredentialBackend {
|
|
|
29
30
|
/** Whether this backend is currently reachable. Sync and cheap. */
|
|
30
31
|
isAvailable(): boolean;
|
|
31
32
|
|
|
32
|
-
/** Retrieve a secret. Returns
|
|
33
|
-
get(account: string): Promise<
|
|
33
|
+
/** Retrieve a secret. Returns a result distinguishing unreachable from not-found. */
|
|
34
|
+
get(account: string): Promise<CredentialGetResult>;
|
|
34
35
|
|
|
35
36
|
/** Store a secret. Returns true on success. */
|
|
36
37
|
set(account: string, value: string): Promise<boolean>;
|
|
@@ -42,79 +43,6 @@ export interface CredentialBackend {
|
|
|
42
43
|
list(): Promise<string[]>;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// KeychainBackend
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
export class KeychainBackend implements CredentialBackend {
|
|
50
|
-
readonly name = "keychain";
|
|
51
|
-
|
|
52
|
-
constructor(private readonly client: KeychainBrokerClient) {}
|
|
53
|
-
|
|
54
|
-
isAvailable(): boolean {
|
|
55
|
-
return this.client.isAvailable();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async get(account: string): Promise<string | undefined> {
|
|
59
|
-
try {
|
|
60
|
-
const result = await this.client.get(account);
|
|
61
|
-
if (result == null) {
|
|
62
|
-
log.warn(
|
|
63
|
-
{ account },
|
|
64
|
-
"Keychain broker unreachable during get — falling back",
|
|
65
|
-
);
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
if (!result.found) return undefined;
|
|
69
|
-
return result.value;
|
|
70
|
-
} catch (err) {
|
|
71
|
-
log.warn({ err, account }, "Keychain get threw unexpectedly");
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async set(account: string, value: string): Promise<boolean> {
|
|
77
|
-
try {
|
|
78
|
-
const result = await this.client.set(account, value);
|
|
79
|
-
if (result.status === "ok") return true;
|
|
80
|
-
log.warn(
|
|
81
|
-
{
|
|
82
|
-
account,
|
|
83
|
-
status: result.status,
|
|
84
|
-
...(result.status === "rejected"
|
|
85
|
-
? { code: result.code, message: result.message }
|
|
86
|
-
: {}),
|
|
87
|
-
},
|
|
88
|
-
"Keychain broker set failed",
|
|
89
|
-
);
|
|
90
|
-
return false;
|
|
91
|
-
} catch (err) {
|
|
92
|
-
log.warn({ err, account }, "Keychain set threw unexpectedly");
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async delete(account: string): Promise<DeleteResult> {
|
|
98
|
-
try {
|
|
99
|
-
const ok = await this.client.del(account);
|
|
100
|
-
// The keychain broker returns a boolean — it does not distinguish
|
|
101
|
-
// "not found" from a genuine error, so we map false → "error".
|
|
102
|
-
return ok ? "deleted" : "error";
|
|
103
|
-
} catch {
|
|
104
|
-
return "error";
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async list(): Promise<string[]> {
|
|
109
|
-
try {
|
|
110
|
-
return await this.client.list();
|
|
111
|
-
} catch (err) {
|
|
112
|
-
log.warn({ err }, "Keychain list threw unexpectedly");
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
46
|
// ---------------------------------------------------------------------------
|
|
119
47
|
// EncryptedStoreBackend
|
|
120
48
|
// ---------------------------------------------------------------------------
|
|
@@ -126,11 +54,11 @@ export class EncryptedStoreBackend implements CredentialBackend {
|
|
|
126
54
|
return true;
|
|
127
55
|
}
|
|
128
56
|
|
|
129
|
-
async get(account: string): Promise<
|
|
57
|
+
async get(account: string): Promise<CredentialGetResult> {
|
|
130
58
|
try {
|
|
131
|
-
return encryptedStore.getKey(account);
|
|
59
|
+
return { value: encryptedStore.getKey(account), unreachable: false };
|
|
132
60
|
} catch {
|
|
133
|
-
return undefined;
|
|
61
|
+
return { value: undefined, unreachable: false };
|
|
134
62
|
}
|
|
135
63
|
}
|
|
136
64
|
|
|
@@ -163,10 +91,6 @@ export class EncryptedStoreBackend implements CredentialBackend {
|
|
|
163
91
|
// Factory functions
|
|
164
92
|
// ---------------------------------------------------------------------------
|
|
165
93
|
|
|
166
|
-
export function createKeychainBackend(): KeychainBackend {
|
|
167
|
-
return new KeychainBackend(createBrokerClient());
|
|
168
|
-
}
|
|
169
|
-
|
|
170
94
|
export function createEncryptedStoreBackend(): EncryptedStoreBackend {
|
|
171
95
|
return new EncryptedStoreBackend();
|
|
172
96
|
}
|