@vellumai/assistant 0.11.1 → 0.11.2-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Typed Slack Web API method wrappers.
|
|
3
3
|
*
|
|
4
4
|
* All methods accept either an OAuthConnection or a raw token string.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Transport (retries, envelope checking, the unified `SlackApiError`) lives
|
|
6
|
+
* in `web-api-transport.ts`.
|
|
7
7
|
*
|
|
8
8
|
* String overloads are retained for non-OAuth callers (e.g. slack/share.ts)
|
|
9
9
|
* that pass raw bot tokens via resolveSlackToken(). These bypass the
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SlackAuthTestResponse,
|
|
17
17
|
SlackBotsInfoResponse,
|
|
18
18
|
SlackConversationHistoryResponse,
|
|
19
|
+
SlackConversationInfoResponse,
|
|
19
20
|
SlackConversationMarkResponse,
|
|
20
21
|
SlackConversationRepliesResponse,
|
|
21
22
|
SlackConversationsListResponse,
|
|
@@ -26,250 +27,7 @@ import type {
|
|
|
26
27
|
SlackUserInfoResponse,
|
|
27
28
|
SlackUsersListResponse,
|
|
28
29
|
} from "./types.js";
|
|
29
|
-
|
|
30
|
-
const SLACK_API_BASE = "https://slack.com/api";
|
|
31
|
-
const MAX_RATE_LIMIT_RETRIES = 3;
|
|
32
|
-
const DEFAULT_RETRY_AFTER_S = 1;
|
|
33
|
-
|
|
34
|
-
export class SlackApiError extends Error {
|
|
35
|
-
constructor(
|
|
36
|
-
public readonly status: number,
|
|
37
|
-
public readonly slackError: string,
|
|
38
|
-
message: string,
|
|
39
|
-
) {
|
|
40
|
-
super(message);
|
|
41
|
-
this.name = "SlackApiError";
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Sleep helper that respects Slack's Retry-After header value.
|
|
47
|
-
*/
|
|
48
|
-
function sleepMs(ms: number): Promise<void> {
|
|
49
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Check a Slack API response envelope for errors and map to SlackApiError.
|
|
54
|
-
* Returns the data if ok, throws otherwise.
|
|
55
|
-
*/
|
|
56
|
-
function checkSlackEnvelope<T extends SlackApiResponse>(data: T): T {
|
|
57
|
-
if (!data.ok) {
|
|
58
|
-
const slackError = data.error ?? "unknown_error";
|
|
59
|
-
const status = [
|
|
60
|
-
"invalid_auth",
|
|
61
|
-
"token_expired",
|
|
62
|
-
"token_revoked",
|
|
63
|
-
"not_authed",
|
|
64
|
-
].includes(slackError)
|
|
65
|
-
? 401
|
|
66
|
-
: 400;
|
|
67
|
-
throw new SlackApiError(
|
|
68
|
-
status,
|
|
69
|
-
slackError,
|
|
70
|
-
`Slack API error: ${slackError}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
return data;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Build a Slack API request using a raw token (for retry via `connection.withToken`).
|
|
78
|
-
*/
|
|
79
|
-
async function rawSlackRequest<T extends SlackApiResponse>(
|
|
80
|
-
token: string,
|
|
81
|
-
method: string,
|
|
82
|
-
query: Record<string, string> | undefined,
|
|
83
|
-
body: Record<string, unknown> | undefined,
|
|
84
|
-
): Promise<T> {
|
|
85
|
-
let url = `${SLACK_API_BASE}/${method}`;
|
|
86
|
-
const headers: Record<string, string> = {
|
|
87
|
-
Authorization: `Bearer ${token}`,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
let init: RequestInit;
|
|
91
|
-
if (body) {
|
|
92
|
-
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
93
|
-
init = { method: "POST", headers, body: JSON.stringify(body) };
|
|
94
|
-
} else {
|
|
95
|
-
if (query && Object.keys(query).length > 0) {
|
|
96
|
-
url += `?${new URLSearchParams(query)}`;
|
|
97
|
-
}
|
|
98
|
-
init = { method: "GET", headers };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const resp = await fetch(url, init);
|
|
102
|
-
if (resp.status === 429) {
|
|
103
|
-
throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
|
|
104
|
-
}
|
|
105
|
-
if (!resp.ok) {
|
|
106
|
-
throw new SlackApiError(
|
|
107
|
-
resp.status,
|
|
108
|
-
`http_${resp.status}`,
|
|
109
|
-
`Slack API HTTP ${resp.status}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
return checkSlackEnvelope((await resp.json()) as T);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Execute a Slack API request via OAuthConnection with rate-limit retry.
|
|
117
|
-
*
|
|
118
|
-
* Slack returns HTTP 200 with `{ ok: false, error: "invalid_auth" }` for
|
|
119
|
-
* auth errors. Because `connection.request()` delegates to `withValidToken`
|
|
120
|
-
* which only retries on HTTP-level 401s, we catch Slack envelope auth
|
|
121
|
-
* errors (mapped to SlackApiError with status 401) and perform a single
|
|
122
|
-
* retry via `connection.withToken()` which forces a token refresh before
|
|
123
|
-
* giving us the new token.
|
|
124
|
-
*/
|
|
125
|
-
async function requestViaConnection<T extends SlackApiResponse>(
|
|
126
|
-
connection: OAuthConnection,
|
|
127
|
-
method: string,
|
|
128
|
-
params?: Record<string, string | undefined>,
|
|
129
|
-
body?: Record<string, unknown>,
|
|
130
|
-
): Promise<T> {
|
|
131
|
-
const query: Record<string, string> | undefined = params
|
|
132
|
-
? Object.fromEntries(
|
|
133
|
-
Object.entries(params).filter(
|
|
134
|
-
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
135
|
-
),
|
|
136
|
-
)
|
|
137
|
-
: undefined;
|
|
138
|
-
|
|
139
|
-
for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
140
|
-
const resp = await connection.request({
|
|
141
|
-
method: body ? "POST" : "GET",
|
|
142
|
-
path: `/${method}`,
|
|
143
|
-
query: query && Object.keys(query).length > 0 ? query : undefined,
|
|
144
|
-
body,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Handle 429 rate limits with Retry-After backoff
|
|
148
|
-
if (resp.status === 429) {
|
|
149
|
-
if (attempt >= MAX_RATE_LIMIT_RETRIES) {
|
|
150
|
-
throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
|
|
151
|
-
}
|
|
152
|
-
const retryAfter =
|
|
153
|
-
parseInt(
|
|
154
|
-
resp.headers["retry-after"] ?? resp.headers["Retry-After"] ?? "",
|
|
155
|
-
10,
|
|
156
|
-
) || DEFAULT_RETRY_AFTER_S;
|
|
157
|
-
await sleepMs(retryAfter * 1000);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (resp.status >= 400) {
|
|
162
|
-
throw new SlackApiError(
|
|
163
|
-
resp.status,
|
|
164
|
-
`http_${resp.status}`,
|
|
165
|
-
`Slack API HTTP ${resp.status}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const data = resp.body as T;
|
|
170
|
-
|
|
171
|
-
// Handle rate_limited error in response body (some Slack APIs return 200 with error)
|
|
172
|
-
if (
|
|
173
|
-
!data.ok &&
|
|
174
|
-
data.error === "rate_limited" &&
|
|
175
|
-
attempt < MAX_RATE_LIMIT_RETRIES
|
|
176
|
-
) {
|
|
177
|
-
await sleepMs(DEFAULT_RETRY_AFTER_S * 1000);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
return checkSlackEnvelope(data);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
// Slack envelope auth errors (invalid_auth, token_expired, etc.) come
|
|
185
|
-
// back as HTTP 200, so they escape withValidToken's retry scope inside
|
|
186
|
-
// connection.request(). Catch them here and retry once with a freshly-
|
|
187
|
-
// refreshed token via connection.withToken().
|
|
188
|
-
if (err instanceof SlackApiError && err.status === 401) {
|
|
189
|
-
return connection.withToken((freshToken) =>
|
|
190
|
-
rawSlackRequest<T>(freshToken, method, query, body),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Unreachable, but TypeScript needs this
|
|
198
|
-
throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Execute a Slack API request via raw token with rate-limit retry.
|
|
203
|
-
*/
|
|
204
|
-
async function requestViaToken<T extends SlackApiResponse>(
|
|
205
|
-
token: string,
|
|
206
|
-
method: string,
|
|
207
|
-
params?: Record<string, string | undefined>,
|
|
208
|
-
body?: Record<string, unknown>,
|
|
209
|
-
): Promise<T> {
|
|
210
|
-
let url = `${SLACK_API_BASE}/${method}`;
|
|
211
|
-
const headers: Record<string, string> = {
|
|
212
|
-
Authorization: `Bearer ${token}`,
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
let init: RequestInit;
|
|
216
|
-
if (body) {
|
|
217
|
-
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
218
|
-
init = { method: "POST", headers, body: JSON.stringify(body) };
|
|
219
|
-
} else {
|
|
220
|
-
if (params) {
|
|
221
|
-
const searchParams = new URLSearchParams();
|
|
222
|
-
for (const [k, v] of Object.entries(params)) {
|
|
223
|
-
if (v !== undefined) {
|
|
224
|
-
searchParams.set(k, v);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
url += `?${searchParams}`;
|
|
228
|
-
}
|
|
229
|
-
init = { method: "GET", headers };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
233
|
-
const resp = await fetch(url, init);
|
|
234
|
-
|
|
235
|
-
// Handle 429 rate limits with Retry-After backoff
|
|
236
|
-
if (resp.status === 429) {
|
|
237
|
-
if (attempt >= MAX_RATE_LIMIT_RETRIES) {
|
|
238
|
-
throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
|
|
239
|
-
}
|
|
240
|
-
const retryAfter =
|
|
241
|
-
parseInt(resp.headers.get("Retry-After") ?? "", 10) ||
|
|
242
|
-
DEFAULT_RETRY_AFTER_S;
|
|
243
|
-
await sleepMs(retryAfter * 1000);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!resp.ok) {
|
|
248
|
-
throw new SlackApiError(
|
|
249
|
-
resp.status,
|
|
250
|
-
`http_${resp.status}`,
|
|
251
|
-
`Slack API HTTP ${resp.status}`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const data = (await resp.json()) as T;
|
|
256
|
-
|
|
257
|
-
// Handle rate_limited error in response body (some Slack APIs return 200 with error)
|
|
258
|
-
if (
|
|
259
|
-
!data.ok &&
|
|
260
|
-
data.error === "rate_limited" &&
|
|
261
|
-
attempt < MAX_RATE_LIMIT_RETRIES
|
|
262
|
-
) {
|
|
263
|
-
await sleepMs(DEFAULT_RETRY_AFTER_S * 1000);
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return checkSlackEnvelope(data);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Unreachable, but TypeScript needs this
|
|
271
|
-
throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
|
|
272
|
-
}
|
|
30
|
+
import { slackRequest } from "./web-api-transport.js";
|
|
273
31
|
|
|
274
32
|
async function request<T extends SlackApiResponse>(
|
|
275
33
|
connectionOrToken: OAuthConnection | string,
|
|
@@ -277,10 +35,10 @@ async function request<T extends SlackApiResponse>(
|
|
|
277
35
|
params?: Record<string, string | undefined>,
|
|
278
36
|
body?: Record<string, unknown>,
|
|
279
37
|
): Promise<T> {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
38
|
+
return slackRequest<T>(connectionOrToken, method, {
|
|
39
|
+
...(params ? { query: params } : {}),
|
|
40
|
+
...(body ? { body } : {}),
|
|
41
|
+
});
|
|
284
42
|
}
|
|
285
43
|
|
|
286
44
|
export async function authTest(
|
|
@@ -397,6 +155,17 @@ export async function conversationsOpen(
|
|
|
397
155
|
);
|
|
398
156
|
}
|
|
399
157
|
|
|
158
|
+
export async function conversationInfo(
|
|
159
|
+
connectionOrToken: OAuthConnection | string,
|
|
160
|
+
channel: string,
|
|
161
|
+
): Promise<SlackConversationInfoResponse> {
|
|
162
|
+
return request<SlackConversationInfoResponse>(
|
|
163
|
+
connectionOrToken,
|
|
164
|
+
"conversations.info",
|
|
165
|
+
{ channel },
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
400
169
|
export async function userInfo(
|
|
401
170
|
connectionOrToken: OAuthConnection | string,
|
|
402
171
|
userId: string,
|
|
@@ -15,14 +15,6 @@ mock.module("./api.js", () => ({
|
|
|
15
15
|
callSlackApiMock(method, body),
|
|
16
16
|
callSlackApiForm: async () => ({}),
|
|
17
17
|
completeSlackUpload: async () => {},
|
|
18
|
-
SlackApiError: class SlackApiError extends Error {
|
|
19
|
-
readonly slackError: string | undefined;
|
|
20
|
-
|
|
21
|
-
constructor(slackError: string | undefined) {
|
|
22
|
-
super(slackError ?? "unknown");
|
|
23
|
-
this.slackError = slackError;
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
18
|
uploadToSlackUrl: async () => {},
|
|
27
19
|
startSlackStream: (params: { markdownText?: string }) =>
|
|
28
20
|
callSlackApiMock("chat.startStream", { ...params }),
|
|
@@ -32,7 +24,10 @@ mock.module("./api.js", () => ({
|
|
|
32
24
|
callSlackApiMock("chat.stopStream", { ...params }),
|
|
33
25
|
}));
|
|
34
26
|
|
|
35
|
-
|
|
27
|
+
// The real error class: send.ts branches on `instanceof SlackApiError` from
|
|
28
|
+
// the (unmocked) shared transport, so thrown test errors must be real
|
|
29
|
+
// instances or every branch under test would silently take the generic path.
|
|
30
|
+
const { SlackApiError } = await import("./web-api-transport.js");
|
|
36
31
|
const { sendSlackAssistantThreadStatus, sendSlackReply } =
|
|
37
32
|
await import("./send.js");
|
|
38
33
|
|
|
@@ -20,12 +20,12 @@ import {
|
|
|
20
20
|
callSlackApi,
|
|
21
21
|
callSlackApiForm,
|
|
22
22
|
completeSlackUpload,
|
|
23
|
-
SlackApiError,
|
|
24
23
|
startSlackStream,
|
|
25
24
|
stopSlackStream,
|
|
26
25
|
uploadToSlackUrl,
|
|
27
26
|
} from "./api.js";
|
|
28
27
|
import { renderSlackBlocks } from "./render.js";
|
|
28
|
+
import { SlackApiError } from "./web-api-transport.js";
|
|
29
29
|
|
|
30
30
|
const log = getLogger("slack-send");
|
|
31
31
|
|
|
@@ -27,6 +27,7 @@ export interface SlackBotsInfoResponse extends SlackApiResponse {
|
|
|
27
27
|
export interface SlackConversation {
|
|
28
28
|
id: string;
|
|
29
29
|
name?: string;
|
|
30
|
+
name_normalized?: string;
|
|
30
31
|
is_channel?: boolean;
|
|
31
32
|
is_group?: boolean;
|
|
32
33
|
is_im?: boolean;
|
|
@@ -45,6 +46,10 @@ export interface SlackConversation {
|
|
|
45
46
|
is_user_deleted?: boolean;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
export interface SlackConversationInfoResponse extends SlackApiResponse {
|
|
50
|
+
channel: SlackConversation;
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
export interface SlackConversationsListResponse extends SlackApiResponse {
|
|
49
54
|
channels: SlackConversation[];
|
|
50
55
|
response_metadata?: { next_cursor?: string };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
checkSlackEnvelope,
|
|
5
|
+
classifySlackError,
|
|
6
|
+
rawSlackRequest,
|
|
7
|
+
SlackApiError,
|
|
8
|
+
} from "./web-api-transport.js";
|
|
9
|
+
|
|
10
|
+
const originalFetch = globalThis.fetch;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
globalThis.fetch = originalFetch;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
17
|
+
return new Response(JSON.stringify(body), {
|
|
18
|
+
status,
|
|
19
|
+
headers: { "content-type": "application/json" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Await a promise expected to reject with SlackApiError and return it. */
|
|
24
|
+
async function captureSlackApiError(
|
|
25
|
+
promise: Promise<unknown>,
|
|
26
|
+
): Promise<SlackApiError> {
|
|
27
|
+
try {
|
|
28
|
+
await promise;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err instanceof SlackApiError) {
|
|
31
|
+
return err;
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
throw new Error("expected the promise to reject with SlackApiError");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("SlackApiError", () => {
|
|
39
|
+
test("derives status from category when not explicit", () => {
|
|
40
|
+
expect(new SlackApiError("invalid_auth").status).toBe(401);
|
|
41
|
+
expect(new SlackApiError("token_expired").status).toBe(401);
|
|
42
|
+
expect(new SlackApiError("rate_limited").status).toBe(429);
|
|
43
|
+
expect(new SlackApiError("ratelimited").status).toBe(429);
|
|
44
|
+
expect(new SlackApiError("channel_not_found").status).toBe(400);
|
|
45
|
+
expect(new SlackApiError("invalid_blocks").status).toBe(400);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("explicit status wins over the derived one", () => {
|
|
49
|
+
expect(new SlackApiError("http_503", { status: 503 }).status).toBe(503);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("normalizes a missing error code to unknown_error", () => {
|
|
53
|
+
const err = new SlackApiError(undefined);
|
|
54
|
+
expect(err.slackError).toBe("unknown_error");
|
|
55
|
+
expect(err.category).toBe("unknown");
|
|
56
|
+
expect(err.message).toBe("Slack API error: unknown_error");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("classifies known Slack error codes", () => {
|
|
60
|
+
expect(new SlackApiError("missing_scope").category).toBe("permission");
|
|
61
|
+
expect(new SlackApiError("is_archived").category).toBe("channel_not_found");
|
|
62
|
+
expect(new SlackApiError("message_not_found").category).toBe("not_found");
|
|
63
|
+
expect(classifySlackError("some_new_code")).toBe("unknown");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("checkSlackEnvelope", () => {
|
|
68
|
+
test("passes ok envelopes through", () => {
|
|
69
|
+
const data = { ok: true as const };
|
|
70
|
+
expect(checkSlackEnvelope(data)).toBe(data);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("throws a classified SlackApiError on ok: false", () => {
|
|
74
|
+
expect(() =>
|
|
75
|
+
checkSlackEnvelope({ ok: false, error: "invalid_auth" }),
|
|
76
|
+
).toThrow(SlackApiError);
|
|
77
|
+
try {
|
|
78
|
+
checkSlackEnvelope({ ok: false, error: "invalid_auth" });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
expect((err as SlackApiError).status).toBe(401);
|
|
81
|
+
expect((err as SlackApiError).category).toBe("auth");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("rawSlackRequest", () => {
|
|
87
|
+
test("retries HTTP 429 honoring Retry-After, then succeeds", async () => {
|
|
88
|
+
let calls = 0;
|
|
89
|
+
globalThis.fetch = mock(async () => {
|
|
90
|
+
calls++;
|
|
91
|
+
if (calls === 1) {
|
|
92
|
+
return new Response("", {
|
|
93
|
+
status: 429,
|
|
94
|
+
headers: { "Retry-After": "0" },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return jsonResponse({ ok: true });
|
|
98
|
+
}) as unknown as typeof fetch;
|
|
99
|
+
|
|
100
|
+
const data = await rawSlackRequest("xoxb-t", "conversations.info", {
|
|
101
|
+
query: { channel: "C1" },
|
|
102
|
+
});
|
|
103
|
+
expect(data.ok).toBe(true);
|
|
104
|
+
expect(calls).toBe(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("retries 5xx and reports SlackApiError after exhausting retries", async () => {
|
|
108
|
+
let calls = 0;
|
|
109
|
+
globalThis.fetch = mock(async () => {
|
|
110
|
+
calls++;
|
|
111
|
+
return new Response("", { status: 503 });
|
|
112
|
+
}) as unknown as typeof fetch;
|
|
113
|
+
|
|
114
|
+
// The fixed 1s backoff between 5xx retries is real time, so this test
|
|
115
|
+
// runs ~3s (3 retries); the timeout below gives it headroom.
|
|
116
|
+
const promise = rawSlackRequest("xoxb-t", "chat.postMessage", {
|
|
117
|
+
body: { channel: "C1", text: "hi" },
|
|
118
|
+
});
|
|
119
|
+
const err = await captureSlackApiError(promise);
|
|
120
|
+
expect(err.status).toBe(503);
|
|
121
|
+
expect(err.slackError).toBe("http_503");
|
|
122
|
+
expect(calls).toBe(4);
|
|
123
|
+
}, 10_000);
|
|
124
|
+
|
|
125
|
+
test("throws classified SlackApiError for non-retryable envelope errors", async () => {
|
|
126
|
+
globalThis.fetch = mock(async () =>
|
|
127
|
+
jsonResponse({ ok: false, error: "channel_not_found" }),
|
|
128
|
+
) as unknown as typeof fetch;
|
|
129
|
+
|
|
130
|
+
const promise = rawSlackRequest("xoxb-t", "conversations.info", {
|
|
131
|
+
query: { channel: "C_MISSING" },
|
|
132
|
+
});
|
|
133
|
+
const err = await captureSlackApiError(promise);
|
|
134
|
+
expect(err.category).toBe("channel_not_found");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("throws http_<status> SlackApiError for non-JSON 4xx responses", async () => {
|
|
138
|
+
globalThis.fetch = mock(
|
|
139
|
+
async () => new Response("forbidden", { status: 403 }),
|
|
140
|
+
) as unknown as typeof fetch;
|
|
141
|
+
|
|
142
|
+
const promise = rawSlackRequest("xoxb-t", "conversations.info", {});
|
|
143
|
+
const err = await captureSlackApiError(promise);
|
|
144
|
+
expect(err.status).toBe(403);
|
|
145
|
+
expect(err.slackError).toBe("http_403");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("rejects form bodies dispatched over an OAuth connection", async () => {
|
|
149
|
+
const connection = {
|
|
150
|
+
request: async () => {
|
|
151
|
+
throw new Error("connection.request must not be reached for forms");
|
|
152
|
+
},
|
|
153
|
+
withToken: async () => {
|
|
154
|
+
throw new Error("withToken must not be reached for forms");
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const { slackRequest } = await import("./web-api-transport.js");
|
|
158
|
+
const err = await captureSlackApiError(
|
|
159
|
+
slackRequest(connection as never, "files.getUploadURLExternal", {
|
|
160
|
+
form: new URLSearchParams({ filename: "a.png" }),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
expect(err.slackError).toBe("form_unsupported_over_oauth");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("sends form bodies as x-www-form-urlencoded POSTs", async () => {
|
|
167
|
+
let capturedInit: RequestInit | undefined;
|
|
168
|
+
globalThis.fetch = mock(async (_input, init) => {
|
|
169
|
+
capturedInit = init;
|
|
170
|
+
return jsonResponse({ ok: true });
|
|
171
|
+
}) as unknown as typeof fetch;
|
|
172
|
+
|
|
173
|
+
await rawSlackRequest("xoxb-t", "files.getUploadURLExternal", {
|
|
174
|
+
form: new URLSearchParams({ filename: "a.png", length: "10" }),
|
|
175
|
+
});
|
|
176
|
+
expect(capturedInit?.method).toBe("POST");
|
|
177
|
+
expect(
|
|
178
|
+
(capturedInit?.headers as Record<string, string>)["Content-Type"],
|
|
179
|
+
).toBe("application/x-www-form-urlencoded");
|
|
180
|
+
});
|
|
181
|
+
});
|