@vellumai/assistant 0.5.10 → 0.5.12
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/AGENTS.md +8 -0
- package/ARCHITECTURE.md +43 -43
- package/Dockerfile +3 -0
- package/docs/architecture/integrations.md +37 -42
- package/docs/architecture/memory.md +7 -12
- package/docs/credential-execution-service.md +9 -9
- package/docs/skills.md +1 -1
- 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/credential-storage/src/index.ts +3 -3
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
- package/openapi.yaml +7208 -0
- package/package.json +2 -1
- package/scripts/generate-openapi.ts +562 -0
- package/src/__tests__/acp-session.test.ts +239 -44
- package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
- package/src/__tests__/browser-skill-endstate.test.ts +1 -1
- package/src/__tests__/btw-routes.test.ts +8 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/channel-approvals.test.ts +7 -7
- package/src/__tests__/channel-readiness-service.test.ts +41 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/config-schema.test.ts +10 -2
- package/src/__tests__/context-memory-e2e.test.ts +2 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/conversation-skill-tools.test.ts +1 -3
- package/src/__tests__/conversation-title-service.test.ts +2 -15
- package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
- package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
- package/src/__tests__/credential-security-e2e.test.ts +4 -4
- package/src/__tests__/credential-security-invariants.test.ts +12 -18
- 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__/dynamic-skill-workflow-prompt.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/heartbeat-service.test.ts +35 -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__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
- package/src/__tests__/log-export-workspace.test.ts +1 -1
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-recall-log-store.test.ts +182 -0
- package/src/__tests__/memory-recall-quality.test.ts +6 -8
- package/src/__tests__/memory-regressions.test.ts +53 -42
- package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/messaging-skill-split.test.ts +2 -17
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +203 -649
- 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-callback-registration.test.ts +119 -0
- package/src/__tests__/secret-ingress-channel.test.ts +261 -0
- package/src/__tests__/secret-ingress-cli.test.ts +201 -0
- package/src/__tests__/secret-ingress-http.test.ts +312 -0
- package/src/__tests__/secret-ingress.test.ts +283 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- 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-integration.test.ts +4 -4
- package/src/__tests__/skill-feature-flags.test.ts +11 -19
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-inline-command.test.ts +3 -3
- package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
- package/src/__tests__/skill-memory.test.ts +2 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
- package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +16 -2
- package/src/__tests__/slack-channel-config.test.ts +1 -1
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/slack-skill.test.ts +5 -69
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
- package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/acp/client-handler.ts +113 -31
- package/src/acp/session-manager.ts +29 -27
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/cli/AGENTS.md +113 -0
- package/src/cli/commands/autonomy.ts +3 -5
- 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 +17 -3
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/memory.ts +2 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +706 -0
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +686 -0
- package/src/cli/commands/oauth/__tests__/mode.test.ts +625 -0
- package/src/cli/commands/oauth/__tests__/ping.test.ts +631 -0
- 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 +551 -0
- package/src/cli/commands/oauth/__tests__/token.test.ts +420 -0
- package/src/cli/commands/oauth/apps.ts +87 -50
- package/src/cli/commands/oauth/connect.ts +405 -0
- package/src/cli/commands/oauth/disconnect.ts +285 -0
- package/src/cli/commands/oauth/index.ts +62 -20
- package/src/cli/commands/oauth/mode.ts +251 -0
- package/src/cli/commands/oauth/ping.ts +196 -0
- package/src/cli/commands/oauth/providers.ts +589 -55
- package/src/cli/commands/oauth/request.ts +564 -0
- package/src/cli/commands/oauth/shared.ts +114 -0
- package/src/cli/commands/oauth/status.ts +191 -0
- package/src/cli/commands/oauth/token.ts +150 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/platform/index.ts +252 -0
- 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/cli.ts +82 -17
- package/src/config/assistant-feature-flags.ts +77 -18
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
- 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 +19 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
- 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/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +2 -2
- package/src/config/bundled-skills/settings/SKILL.md +5 -3
- package/src/config/bundled-skills/settings/TOOLS.json +17 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
- package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
- package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
- package/src/config/bundled-skills/slack/SKILL.md +58 -44
- package/src/config/bundled-tool-registry.ts +7 -19
- package/src/config/env.ts +5 -1
- package/src/config/feature-flag-registry.json +58 -42
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/security.ts +9 -1
- package/src/config/schemas/services.ts +1 -1
- package/src/config/skill-state.ts +1 -3
- package/src/config/skills.ts +2 -4
- package/src/credential-execution/client.ts +1 -1
- package/src/credential-execution/feature-gates.ts +9 -16
- package/src/credential-execution/process-manager.ts +12 -0
- package/src/daemon/config-watcher.ts +4 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
- package/src/daemon/conversation-agent-loop.ts +51 -2
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-memory.ts +0 -1
- 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/handlers/config-slack-channel.ts +43 -1
- package/src/daemon/handlers/conversations.ts +41 -33
- 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 +73 -3
- package/src/daemon/message-types/acp.ts +0 -15
- 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/message-types/memory.ts +0 -1
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +9 -0
- package/src/daemon/server.ts +48 -9
- package/src/email/feature-gate.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +48 -0
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +68 -19
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/mcp/mcp-oauth-provider.ts +3 -3
- package/src/memory/app-store.ts +3 -3
- package/src/memory/conversation-crud.ts +213 -0
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-title-service.ts +7 -17
- package/src/memory/db-init.ts +24 -0
- package/src/memory/embedding-local.ts +47 -2
- package/src/memory/indexer.ts +13 -10
- package/src/memory/items-extractor.ts +12 -4
- package/src/memory/job-utils.ts +5 -0
- package/src/memory/jobs-store.ts +10 -2
- package/src/memory/journal-memory.ts +6 -2
- package/src/memory/llm-request-log-store.ts +88 -21
- package/src/memory/memory-recall-log-store.ts +128 -0
- package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
- package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
- 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 +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +4 -5
- package/src/memory/schema/infrastructure.ts +31 -0
- package/src/memory/schema/oauth.ts +14 -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 +7 -9
- 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 +79 -64
- 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/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +251 -5
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +256 -34
- package/src/permissions/checker.ts +129 -3
- package/src/permissions/trust-client.ts +2 -2
- package/src/platform/client.ts +2 -2
- package/src/prompts/journal-context.ts +6 -1
- package/src/prompts/system-prompt.ts +43 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +139 -28
- package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
- package/src/runtime/auth/route-policy.ts +0 -1
- package/src/runtime/btw-sidechain.ts +7 -1
- package/src/runtime/channel-approvals.ts +2 -2
- package/src/runtime/channel-readiness-service.ts +30 -7
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-router.ts +31 -0
- package/src/runtime/http-server.ts +26 -7
- package/src/runtime/http-types.ts +9 -0
- package/src/runtime/pending-interactions.ts +21 -3
- package/src/runtime/routes/acp-routes.ts +46 -28
- package/src/runtime/routes/app-management-routes.ts +123 -0
- package/src/runtime/routes/app-routes.ts +31 -0
- package/src/runtime/routes/approval-routes.ts +108 -3
- package/src/runtime/routes/attachment-routes.ts +45 -0
- package/src/runtime/routes/avatar-routes.ts +16 -0
- package/src/runtime/routes/brain-graph-routes.ts +18 -0
- package/src/runtime/routes/btw-routes.ts +20 -0
- package/src/runtime/routes/call-routes.ts +81 -0
- package/src/runtime/routes/channel-readiness-routes.ts +48 -7
- package/src/runtime/routes/channel-routes.ts +18 -0
- package/src/runtime/routes/channel-verification-routes.ts +49 -1
- package/src/runtime/routes/contact-routes.ts +77 -0
- package/src/runtime/routes/conversation-attention-routes.ts +37 -0
- package/src/runtime/routes/conversation-management-routes.ts +125 -0
- package/src/runtime/routes/conversation-query-routes.ts +78 -0
- package/src/runtime/routes/conversation-routes.ts +191 -39
- package/src/runtime/routes/conversation-starter-routes.ts +29 -0
- package/src/runtime/routes/debug-routes.ts +23 -0
- package/src/runtime/routes/diagnostics-routes.ts +30 -0
- package/src/runtime/routes/documents-routes.ts +42 -0
- package/src/runtime/routes/events-routes.ts +10 -0
- package/src/runtime/routes/global-search-routes.ts +35 -0
- package/src/runtime/routes/guardian-action-routes.ts +61 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
- package/src/runtime/routes/heartbeat-routes.ts +278 -0
- package/src/runtime/routes/host-bash-routes.ts +16 -1
- package/src/runtime/routes/host-cu-routes.ts +23 -1
- package/src/runtime/routes/host-file-routes.ts +18 -1
- package/src/runtime/routes/identity-routes.ts +35 -0
- package/src/runtime/routes/inbound-message-handler.ts +46 -25
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/integrations/twilio.ts +32 -22
- package/src/runtime/routes/invite-routes.ts +83 -0
- package/src/runtime/routes/log-export-routes.ts +14 -0
- package/src/runtime/routes/memory-item-routes.ts +99 -1
- package/src/runtime/routes/migration-rollback-routes.ts +25 -0
- package/src/runtime/routes/migration-routes.ts +40 -0
- package/src/runtime/routes/notification-routes.ts +20 -0
- package/src/runtime/routes/oauth-apps.ts +13 -4
- package/src/runtime/routes/pairing-routes.ts +15 -0
- package/src/runtime/routes/recording-routes.ts +72 -0
- package/src/runtime/routes/schedule-routes.ts +77 -5
- package/src/runtime/routes/secret-routes.ts +99 -14
- package/src/runtime/routes/settings-routes.ts +102 -19
- package/src/runtime/routes/skills-routes.ts +141 -18
- package/src/runtime/routes/subagents-routes.ts +38 -3
- package/src/runtime/routes/surface-action-routes.ts +66 -24
- package/src/runtime/routes/surface-content-routes.ts +20 -0
- package/src/runtime/routes/telemetry-routes.ts +12 -0
- package/src/runtime/routes/trace-event-routes.ts +25 -0
- package/src/runtime/routes/trust-rules-routes.ts +46 -0
- package/src/runtime/routes/tts-routes.ts +15 -4
- package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
- package/src/runtime/routes/usage-routes.ts +59 -0
- package/src/runtime/routes/watch-routes.ts +28 -0
- package/src/runtime/routes/work-items-routes.ts +59 -0
- package/src/runtime/routes/workspace-commit-routes.ts +12 -0
- package/src/runtime/routes/workspace-routes.ts +102 -0
- package/src/schedule/integration-status.ts +2 -2
- package/src/schedule/scheduler.ts +7 -1
- package/src/security/AGENTS.md +7 -0
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/credential-backend.ts +1 -1
- package/src/security/encrypted-store.ts +3 -3
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +58 -17
- package/src/security/secret-ingress.ts +174 -0
- package/src/security/secret-patterns.ts +133 -0
- package/src/security/secret-scanner.ts +28 -117
- 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/signals/confirm.ts +12 -8
- package/src/signals/user-message.ts +18 -3
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/skills/skill-memory.ts +1 -2
- package/src/tasks/task-runner.ts +7 -1
- package/src/tools/credentials/broker.ts +1 -1
- package/src/tools/credentials/metadata-store.ts +1 -1
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +36 -48
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/definitions.ts +1 -1
- package/src/tools/memory/handlers.test.ts +2 -4
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/terminal/shell.ts +16 -3
- package/src/tools/tool-manifest.ts +1 -1
- package/src/util/log-redact.ts +9 -34
- package/src/util/logger.ts +11 -1
- 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/docs/architecture/keychain-broker.md +0 -68
- package/src/cli/commands/oauth/connections.ts +0 -734
- package/src/cli/commands/oauth/platform.ts +0 -525
- package/src/cli/commands/platform.ts +0 -176
- package/src/config/bundled-skills/slack/TOOLS.json +0 -272
- package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* endpoint. Delivery is proxied through the gateway which owns the Meta Cloud API
|
|
6
6
|
* credentials (phone_number_id + access_token).
|
|
7
7
|
*
|
|
8
|
-
* The `
|
|
8
|
+
* The `connection` parameter in MessagingProvider methods is unused
|
|
9
9
|
* for WhatsApp because delivery is authenticated via the gateway's bearer
|
|
10
10
|
* token, not a per-user OAuth token.
|
|
11
11
|
*/
|
|
@@ -66,9 +66,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
|
|
|
66
66
|
return hasWhatsAppCredentials();
|
|
67
67
|
},
|
|
68
68
|
|
|
69
|
-
async testConnection(
|
|
70
|
-
_connectionOrToken: OAuthConnection | string,
|
|
71
|
-
): Promise<ConnectionInfo> {
|
|
69
|
+
async testConnection(_connection?: OAuthConnection): Promise<ConnectionInfo> {
|
|
72
70
|
if (!(await hasWhatsAppCredentials())) {
|
|
73
71
|
return {
|
|
74
72
|
connected: false,
|
|
@@ -96,7 +94,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
|
|
|
96
94
|
},
|
|
97
95
|
|
|
98
96
|
async sendMessage(
|
|
99
|
-
|
|
97
|
+
_connection: OAuthConnection | undefined,
|
|
100
98
|
conversationId: string,
|
|
101
99
|
text: string,
|
|
102
100
|
options?: SendOptions,
|
|
@@ -140,7 +138,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
|
|
|
140
138
|
|
|
141
139
|
// WhatsApp does not support listing conversations via this provider.
|
|
142
140
|
async listConversations(
|
|
143
|
-
|
|
141
|
+
_connection?: OAuthConnection,
|
|
144
142
|
_options?: ListOptions,
|
|
145
143
|
): Promise<Conversation[]> {
|
|
146
144
|
return [];
|
|
@@ -148,7 +146,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
|
|
|
148
146
|
|
|
149
147
|
// WhatsApp does not provide message history retrieval via the gateway.
|
|
150
148
|
async getHistory(
|
|
151
|
-
|
|
149
|
+
_connection: OAuthConnection | undefined,
|
|
152
150
|
_conversationId: string,
|
|
153
151
|
_options?: HistoryOptions,
|
|
154
152
|
): Promise<Message[]> {
|
|
@@ -157,7 +155,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
|
|
|
157
155
|
|
|
158
156
|
// WhatsApp does not support message search.
|
|
159
157
|
async search(
|
|
160
|
-
|
|
158
|
+
_connection: OAuthConnection | undefined,
|
|
161
159
|
_query: string,
|
|
162
160
|
_options?: SearchOptions,
|
|
163
161
|
): Promise<SearchResult> {
|
|
@@ -5,14 +5,20 @@
|
|
|
5
5
|
* Follows the same delivery pattern used by guardian-dispatch: POST to
|
|
6
6
|
* the gateway's `/deliver/telegram` endpoint with a chat ID and text
|
|
7
7
|
* payload. The gateway forwards the message to the Telegram Bot API.
|
|
8
|
+
*
|
|
9
|
+
* For access request notifications, inline keyboard buttons ("Approve once",
|
|
10
|
+
* "Reject") are attached via the approval payload so the guardian can act
|
|
11
|
+
* without typing a command. If the rich delivery fails, the adapter falls
|
|
12
|
+
* back to plain text with typed-command instructions.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { getGatewayInternalBaseUrl } from "../../config/env.js";
|
|
11
16
|
import { mintDaemonDeliveryToken } from "../../runtime/auth/token-service.js";
|
|
17
|
+
import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
|
|
12
18
|
import { deliverChannelReply } from "../../runtime/gateway-client.js";
|
|
13
19
|
import { getLogger } from "../../util/logger.js";
|
|
14
20
|
import { isConversationSeedSane } from "../conversation-seed-composer.js";
|
|
15
|
-
import { nonEmpty } from "../copy-composer.js";
|
|
21
|
+
import { buildAccessRequestContractText, nonEmpty } from "../copy-composer.js";
|
|
16
22
|
import type {
|
|
17
23
|
ChannelAdapter,
|
|
18
24
|
ChannelDeliveryPayload,
|
|
@@ -40,6 +46,34 @@ function resolveTelegramMessageText(payload: ChannelDeliveryPayload): string {
|
|
|
40
46
|
return payload.sourceEventName.replace(/[._]/g, " ");
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Build an {@link ApprovalUIMetadata} for an access request so the gateway
|
|
51
|
+
* renders inline keyboard buttons in the Telegram message.
|
|
52
|
+
*
|
|
53
|
+
* Returns `undefined` when the context payload is missing the required
|
|
54
|
+
* `requestId`, in which case the caller should fall back to plain text.
|
|
55
|
+
*/
|
|
56
|
+
function buildAccessRequestApproval(
|
|
57
|
+
contextPayload: Record<string, unknown>,
|
|
58
|
+
): ApprovalUIMetadata | undefined {
|
|
59
|
+
const requestId =
|
|
60
|
+
typeof contextPayload.requestId === "string"
|
|
61
|
+
? contextPayload.requestId
|
|
62
|
+
: undefined;
|
|
63
|
+
if (!requestId) return undefined;
|
|
64
|
+
|
|
65
|
+
const plainTextFallback = buildAccessRequestContractText(contextPayload);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
requestId,
|
|
69
|
+
actions: [
|
|
70
|
+
{ id: "approve_once", label: "Approve once" },
|
|
71
|
+
{ id: "reject", label: "Reject" },
|
|
72
|
+
],
|
|
73
|
+
plainTextFallback,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
export class TelegramAdapter implements ChannelAdapter {
|
|
44
78
|
readonly channel: NotificationChannel = "telegram";
|
|
45
79
|
|
|
@@ -66,10 +100,52 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
66
100
|
// delivery copy when available and avoid deterministic label prefixes.
|
|
67
101
|
const messageText = resolveTelegramMessageText(payload);
|
|
68
102
|
|
|
103
|
+
// For access requests, attach inline keyboard buttons so the guardian
|
|
104
|
+
// can approve/reject with a single tap.
|
|
105
|
+
const isAccessRequest =
|
|
106
|
+
payload.sourceEventName === "ingress.access_request" &&
|
|
107
|
+
payload.contextPayload != null;
|
|
108
|
+
|
|
109
|
+
const approval = isAccessRequest
|
|
110
|
+
? buildAccessRequestApproval(payload.contextPayload!)
|
|
111
|
+
: undefined;
|
|
112
|
+
|
|
69
113
|
try {
|
|
114
|
+
if (approval) {
|
|
115
|
+
// Attempt rich delivery with inline keyboard buttons.
|
|
116
|
+
// On failure, fall back to plain text below.
|
|
117
|
+
try {
|
|
118
|
+
await deliverChannelReply(
|
|
119
|
+
deliverUrl,
|
|
120
|
+
{ chatId, text: messageText, approval },
|
|
121
|
+
mintDaemonDeliveryToken(),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
log.info(
|
|
125
|
+
{ sourceEventName: payload.sourceEventName, chatId },
|
|
126
|
+
"Telegram access request notification delivered with inline buttons",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return { success: true };
|
|
130
|
+
} catch (richErr) {
|
|
131
|
+
log.warn(
|
|
132
|
+
{ err: richErr, sourceEventName: payload.sourceEventName, chatId },
|
|
133
|
+
"Rich Telegram delivery failed — falling back to plain text",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// When falling back from rich delivery, append the plain-text
|
|
139
|
+
// instructions so the guardian still knows how to approve/reject.
|
|
140
|
+
const fallbackText =
|
|
141
|
+
approval?.plainTextFallback &&
|
|
142
|
+
!messageText.includes(approval.plainTextFallback)
|
|
143
|
+
? `${messageText}\n\n${approval.plainTextFallback}`
|
|
144
|
+
: messageText;
|
|
145
|
+
|
|
70
146
|
await deliverChannelReply(
|
|
71
147
|
deliverUrl,
|
|
72
|
-
{ chatId, text:
|
|
148
|
+
{ chatId, text: fallbackText },
|
|
73
149
|
mintDaemonDeliveryToken(),
|
|
74
150
|
);
|
|
75
151
|
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { verifyIdentity } from "../identity-verifier.js";
|
|
4
|
+
import type { OAuthProviderRow } from "../oauth-store.js";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Mock fetch
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const originalFetch = globalThis.fetch;
|
|
11
|
+
let mockFetch: ReturnType<typeof mock<any>>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockFetch = mock(() => Promise.resolve(new Response("{}", { status: 200 })));
|
|
15
|
+
globalThis.fetch = mockFetch as unknown as typeof fetch;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
globalThis.fetch = originalFetch;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helper: build a minimal OAuthProviderRow with identity fields
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function makeProviderRow(
|
|
27
|
+
overrides: Partial<OAuthProviderRow> & { providerKey: string },
|
|
28
|
+
): OAuthProviderRow {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const { providerKey, ...rest } = overrides;
|
|
31
|
+
return {
|
|
32
|
+
providerKey,
|
|
33
|
+
authUrl: "https://example.com/auth",
|
|
34
|
+
tokenUrl: "https://example.com/token",
|
|
35
|
+
tokenEndpointAuthMethod: null,
|
|
36
|
+
userinfoUrl: null,
|
|
37
|
+
baseUrl: null,
|
|
38
|
+
defaultScopes: "[]",
|
|
39
|
+
scopePolicy: "{}",
|
|
40
|
+
extraParams: null,
|
|
41
|
+
callbackTransport: null,
|
|
42
|
+
pingUrl: null,
|
|
43
|
+
pingMethod: null,
|
|
44
|
+
pingHeaders: null,
|
|
45
|
+
pingBody: null,
|
|
46
|
+
managedServiceConfigKey: null,
|
|
47
|
+
displayName: null,
|
|
48
|
+
description: null,
|
|
49
|
+
dashboardUrl: null,
|
|
50
|
+
clientIdPlaceholder: null,
|
|
51
|
+
requiresClientSecret: 1,
|
|
52
|
+
loopbackPort: null,
|
|
53
|
+
injectionTemplates: null,
|
|
54
|
+
appType: null,
|
|
55
|
+
setupNotes: null,
|
|
56
|
+
identityUrl: null,
|
|
57
|
+
identityMethod: null,
|
|
58
|
+
identityHeaders: null,
|
|
59
|
+
identityBody: null,
|
|
60
|
+
identityResponsePaths: null,
|
|
61
|
+
identityFormat: null,
|
|
62
|
+
identityOkField: null,
|
|
63
|
+
createdAt: now,
|
|
64
|
+
updatedAt: now,
|
|
65
|
+
...rest,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
70
|
+
return new Response(JSON.stringify(body), {
|
|
71
|
+
status,
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Tests
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe("verifyIdentity", () => {
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
// Missing identity URL
|
|
83
|
+
// -----------------------------------------------------------------------
|
|
84
|
+
test("returns undefined when identityUrl is null", async () => {
|
|
85
|
+
const row = makeProviderRow({ providerKey: "custom" });
|
|
86
|
+
const result = await verifyIdentity(row, "token-abc");
|
|
87
|
+
expect(result).toBeUndefined();
|
|
88
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// -----------------------------------------------------------------------
|
|
92
|
+
// Google: simple GET, extract email
|
|
93
|
+
// -----------------------------------------------------------------------
|
|
94
|
+
describe("Google pattern", () => {
|
|
95
|
+
const googleRow = makeProviderRow({
|
|
96
|
+
providerKey: "google",
|
|
97
|
+
identityUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
98
|
+
identityResponsePaths: JSON.stringify(["email"]),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("extracts email from response", async () => {
|
|
102
|
+
mockFetch.mockResolvedValueOnce(
|
|
103
|
+
jsonResponse({ email: "user@gmail.com", name: "Test User" }),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const result = await verifyIdentity(googleRow, "google-token");
|
|
107
|
+
|
|
108
|
+
expect(result).toBe("user@gmail.com");
|
|
109
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
110
|
+
|
|
111
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
112
|
+
expect(url).toBe("https://www.googleapis.com/oauth2/v2/userinfo");
|
|
113
|
+
expect((init as RequestInit).method).toBe("GET");
|
|
114
|
+
const headers = (init as RequestInit).headers as Record<string, string>;
|
|
115
|
+
expect(headers["Authorization"]).toBe("Bearer google-token");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns undefined when email is missing", async () => {
|
|
119
|
+
mockFetch.mockResolvedValueOnce(jsonResponse({ name: "Test User" }));
|
|
120
|
+
const result = await verifyIdentity(googleRow, "google-token");
|
|
121
|
+
expect(result).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
// Slack: GET with ok check + format template
|
|
127
|
+
// -----------------------------------------------------------------------
|
|
128
|
+
describe("Slack pattern", () => {
|
|
129
|
+
const slackRow = makeProviderRow({
|
|
130
|
+
providerKey: "slack",
|
|
131
|
+
identityUrl: "https://slack.com/api/auth.test",
|
|
132
|
+
identityOkField: "ok",
|
|
133
|
+
identityResponsePaths: JSON.stringify(["user", "team"]),
|
|
134
|
+
identityFormat: "@${user} (${team})",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("returns formatted string when all fields present", async () => {
|
|
138
|
+
mockFetch.mockResolvedValueOnce(
|
|
139
|
+
jsonResponse({ ok: true, user: "alice", team: "acme-corp" }),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const result = await verifyIdentity(slackRow, "slack-token");
|
|
143
|
+
expect(result).toBe("@alice (acme-corp)");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("returns @user when team is missing (fallback)", async () => {
|
|
147
|
+
mockFetch.mockResolvedValueOnce(
|
|
148
|
+
jsonResponse({ ok: true, user: "alice" }),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const result = await verifyIdentity(slackRow, "slack-token");
|
|
152
|
+
expect(result).toBe("@alice");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("returns undefined when ok is false", async () => {
|
|
156
|
+
mockFetch.mockResolvedValueOnce(
|
|
157
|
+
jsonResponse({ ok: false, user: "alice", team: "acme-corp" }),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const result = await verifyIdentity(slackRow, "slack-token");
|
|
161
|
+
expect(result).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("returns undefined when ok field is missing", async () => {
|
|
165
|
+
mockFetch.mockResolvedValueOnce(
|
|
166
|
+
jsonResponse({ user: "alice", team: "acme-corp" }),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const result = await verifyIdentity(slackRow, "slack-token");
|
|
170
|
+
expect(result).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// -----------------------------------------------------------------------
|
|
175
|
+
// HubSpot: URL-interpolated token, no Authorization header
|
|
176
|
+
// -----------------------------------------------------------------------
|
|
177
|
+
describe("HubSpot pattern", () => {
|
|
178
|
+
const hubspotRow = makeProviderRow({
|
|
179
|
+
providerKey: "hubspot",
|
|
180
|
+
identityUrl:
|
|
181
|
+
"https://api.hubapi.com/oauth/v1/access-tokens/${accessToken}",
|
|
182
|
+
identityResponsePaths: JSON.stringify(["user", "hub_domain"]),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("interpolates token in URL and skips Authorization header", async () => {
|
|
186
|
+
mockFetch.mockResolvedValueOnce(
|
|
187
|
+
jsonResponse({
|
|
188
|
+
user: "admin@hubspot.com",
|
|
189
|
+
hub_domain: "mycompany.hubspot.com",
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const result = await verifyIdentity(hubspotRow, "hs-token-123");
|
|
194
|
+
|
|
195
|
+
expect(result).toBe("admin@hubspot.com");
|
|
196
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
197
|
+
|
|
198
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
199
|
+
expect(url).toBe(
|
|
200
|
+
"https://api.hubapi.com/oauth/v1/access-tokens/hs-token-123",
|
|
201
|
+
);
|
|
202
|
+
const headers = (init as RequestInit).headers as Record<string, string>;
|
|
203
|
+
// Should NOT have Authorization header since token is in URL
|
|
204
|
+
expect(headers["Authorization"]).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("falls back to hub_domain when user is missing", async () => {
|
|
208
|
+
mockFetch.mockResolvedValueOnce(
|
|
209
|
+
jsonResponse({ hub_domain: "mycompany.hubspot.com" }),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const result = await verifyIdentity(hubspotRow, "hs-token-123");
|
|
213
|
+
expect(result).toBe("mycompany.hubspot.com");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// -----------------------------------------------------------------------
|
|
218
|
+
// Linear: POST with JSON body, GraphQL response
|
|
219
|
+
// -----------------------------------------------------------------------
|
|
220
|
+
describe("Linear pattern", () => {
|
|
221
|
+
const linearRow = makeProviderRow({
|
|
222
|
+
providerKey: "linear",
|
|
223
|
+
identityUrl: "https://api.linear.app/graphql",
|
|
224
|
+
identityMethod: "POST",
|
|
225
|
+
identityHeaders: JSON.stringify({ "Content-Type": "application/json" }),
|
|
226
|
+
identityBody: JSON.stringify({
|
|
227
|
+
query: "{ viewer { email name } }",
|
|
228
|
+
}),
|
|
229
|
+
identityResponsePaths: JSON.stringify([
|
|
230
|
+
"data.viewer.email",
|
|
231
|
+
"data.viewer.name",
|
|
232
|
+
]),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("extracts email from nested GraphQL response", async () => {
|
|
236
|
+
mockFetch.mockResolvedValueOnce(
|
|
237
|
+
jsonResponse({
|
|
238
|
+
data: { viewer: { email: "dev@linear.app", name: "Dev User" } },
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const result = await verifyIdentity(linearRow, "linear-token");
|
|
243
|
+
|
|
244
|
+
expect(result).toBe("dev@linear.app");
|
|
245
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
246
|
+
|
|
247
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
248
|
+
expect(url).toBe("https://api.linear.app/graphql");
|
|
249
|
+
expect((init as RequestInit).method).toBe("POST");
|
|
250
|
+
const headers = (init as RequestInit).headers as Record<string, string>;
|
|
251
|
+
expect(headers["Authorization"]).toBe("Bearer linear-token");
|
|
252
|
+
expect(headers["Content-Type"]).toBe("application/json");
|
|
253
|
+
expect((init as RequestInit).body).toBe(
|
|
254
|
+
JSON.stringify({ query: "{ viewer { email name } }" }),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("falls back to name when email is missing", async () => {
|
|
259
|
+
mockFetch.mockResolvedValueOnce(
|
|
260
|
+
jsonResponse({
|
|
261
|
+
data: { viewer: { name: "Dev User" } },
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const result = await verifyIdentity(linearRow, "linear-token");
|
|
266
|
+
expect(result).toBe("Dev User");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// -----------------------------------------------------------------------
|
|
271
|
+
// Todoist: POST with form body
|
|
272
|
+
// -----------------------------------------------------------------------
|
|
273
|
+
describe("Todoist pattern", () => {
|
|
274
|
+
const todoistRow = makeProviderRow({
|
|
275
|
+
providerKey: "todoist",
|
|
276
|
+
identityUrl: "https://api.todoist.com/sync/v9/sync",
|
|
277
|
+
identityMethod: "POST",
|
|
278
|
+
identityHeaders: JSON.stringify({
|
|
279
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
280
|
+
}),
|
|
281
|
+
identityBody: JSON.stringify("sync_token=*&resource_types=[%22user%22]"),
|
|
282
|
+
identityResponsePaths: JSON.stringify(["user.full_name", "user.email"]),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("extracts full_name from nested response", async () => {
|
|
286
|
+
mockFetch.mockResolvedValueOnce(
|
|
287
|
+
jsonResponse({
|
|
288
|
+
user: { full_name: "Jane Doe", email: "jane@example.com" },
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const result = await verifyIdentity(todoistRow, "todoist-token");
|
|
293
|
+
|
|
294
|
+
expect(result).toBe("Jane Doe");
|
|
295
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
296
|
+
|
|
297
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
298
|
+
expect((init as RequestInit).method).toBe("POST");
|
|
299
|
+
// Body should be the form-encoded string
|
|
300
|
+
expect((init as RequestInit).body).toBe(
|
|
301
|
+
"sync_token=*&resource_types=[%22user%22]",
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("falls back to email when full_name is missing", async () => {
|
|
306
|
+
mockFetch.mockResolvedValueOnce(
|
|
307
|
+
jsonResponse({ user: { email: "jane@example.com" } }),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const result = await verifyIdentity(todoistRow, "todoist-token");
|
|
311
|
+
expect(result).toBe("jane@example.com");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// -----------------------------------------------------------------------
|
|
316
|
+
// Twitter: format template with nested path
|
|
317
|
+
// -----------------------------------------------------------------------
|
|
318
|
+
describe("Twitter pattern", () => {
|
|
319
|
+
const twitterRow = makeProviderRow({
|
|
320
|
+
providerKey: "twitter",
|
|
321
|
+
identityUrl: "https://api.x.com/2/users/me",
|
|
322
|
+
identityResponsePaths: JSON.stringify(["data.username"]),
|
|
323
|
+
identityFormat: "@${data.username}",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("returns formatted @username", async () => {
|
|
327
|
+
mockFetch.mockResolvedValueOnce(
|
|
328
|
+
jsonResponse({ data: { username: "elonmusk" } }),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const result = await verifyIdentity(twitterRow, "twitter-token");
|
|
332
|
+
expect(result).toBe("@elonmusk");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// -----------------------------------------------------------------------
|
|
337
|
+
// GitHub: format template with simple path
|
|
338
|
+
// -----------------------------------------------------------------------
|
|
339
|
+
describe("GitHub pattern", () => {
|
|
340
|
+
const githubRow = makeProviderRow({
|
|
341
|
+
providerKey: "github",
|
|
342
|
+
identityUrl: "https://api.github.com/user",
|
|
343
|
+
identityResponsePaths: JSON.stringify(["login"]),
|
|
344
|
+
identityFormat: "@${login}",
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("returns formatted @login", async () => {
|
|
348
|
+
mockFetch.mockResolvedValueOnce(jsonResponse({ login: "octocat" }));
|
|
349
|
+
|
|
350
|
+
const result = await verifyIdentity(githubRow, "gh-token");
|
|
351
|
+
expect(result).toBe("@octocat");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// -----------------------------------------------------------------------
|
|
356
|
+
// Notion: custom headers, multiple fallback paths
|
|
357
|
+
// -----------------------------------------------------------------------
|
|
358
|
+
describe("Notion pattern", () => {
|
|
359
|
+
const notionRow = makeProviderRow({
|
|
360
|
+
providerKey: "notion",
|
|
361
|
+
identityUrl: "https://api.notion.com/v1/users/me",
|
|
362
|
+
identityHeaders: JSON.stringify({ "Notion-Version": "2022-06-28" }),
|
|
363
|
+
identityResponsePaths: JSON.stringify(["name", "person.email"]),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("returns name when present", async () => {
|
|
367
|
+
mockFetch.mockResolvedValueOnce(
|
|
368
|
+
jsonResponse({ name: "Test Bot", person: { email: "user@notion.so" } }),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const result = await verifyIdentity(notionRow, "notion-token");
|
|
372
|
+
expect(result).toBe("Test Bot");
|
|
373
|
+
|
|
374
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
375
|
+
const headers = (init as RequestInit).headers as Record<string, string>;
|
|
376
|
+
expect(headers["Notion-Version"]).toBe("2022-06-28");
|
|
377
|
+
expect(headers["Authorization"]).toBe("Bearer notion-token");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("falls back to person.email when name is missing", async () => {
|
|
381
|
+
mockFetch.mockResolvedValueOnce(
|
|
382
|
+
jsonResponse({ person: { email: "user@notion.so" } }),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const result = await verifyIdentity(notionRow, "notion-token");
|
|
386
|
+
expect(result).toBe("user@notion.so");
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// -----------------------------------------------------------------------
|
|
391
|
+
// Error handling
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
describe("error handling", () => {
|
|
394
|
+
const googleRow = makeProviderRow({
|
|
395
|
+
providerKey: "google",
|
|
396
|
+
identityUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
397
|
+
identityResponsePaths: JSON.stringify(["email"]),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("returns undefined on fetch failure", async () => {
|
|
401
|
+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
402
|
+
|
|
403
|
+
const result = await verifyIdentity(googleRow, "token");
|
|
404
|
+
expect(result).toBeUndefined();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("returns undefined on non-OK response", async () => {
|
|
408
|
+
mockFetch.mockResolvedValueOnce(
|
|
409
|
+
new Response("Unauthorized", { status: 401 }),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const result = await verifyIdentity(googleRow, "token");
|
|
413
|
+
expect(result).toBeUndefined();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("returns undefined on invalid JSON response", async () => {
|
|
417
|
+
mockFetch.mockResolvedValueOnce(
|
|
418
|
+
new Response("not json", {
|
|
419
|
+
status: 200,
|
|
420
|
+
headers: { "Content-Type": "application/json" },
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const result = await verifyIdentity(googleRow, "token");
|
|
425
|
+
expect(result).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// -----------------------------------------------------------------------
|
|
430
|
+
// Dropbox: POST with no explicit body
|
|
431
|
+
// -----------------------------------------------------------------------
|
|
432
|
+
describe("Dropbox pattern", () => {
|
|
433
|
+
const dropboxRow = makeProviderRow({
|
|
434
|
+
providerKey: "dropbox",
|
|
435
|
+
identityUrl: "https://api.dropboxapi.com/2/users/get_current_account",
|
|
436
|
+
identityMethod: "POST",
|
|
437
|
+
identityResponsePaths: JSON.stringify(["name.display_name", "email"]),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("extracts nested display_name", async () => {
|
|
441
|
+
mockFetch.mockResolvedValueOnce(
|
|
442
|
+
jsonResponse({
|
|
443
|
+
name: { display_name: "Jane Doe" },
|
|
444
|
+
email: "jane@dropbox.com",
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const result = await verifyIdentity(dropboxRow, "dbx-token");
|
|
449
|
+
expect(result).toBe("Jane Doe");
|
|
450
|
+
|
|
451
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
452
|
+
expect((init as RequestInit).method).toBe("POST");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("falls back to email when name is missing", async () => {
|
|
456
|
+
mockFetch.mockResolvedValueOnce(
|
|
457
|
+
jsonResponse({ email: "jane@dropbox.com" }),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const result = await verifyIdentity(dropboxRow, "dbx-token");
|
|
461
|
+
expect(result).toBe("jane@dropbox.com");
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|