@vellumai/assistant 0.11.5 → 0.11.6-staging.1
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 +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -30,7 +30,8 @@ mock.module("./api.js", () => ({
|
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
32
|
const { TelegramNonRetryableError } = await import("./api.js");
|
|
33
|
-
const { sendTelegramReply, sendTelegramRichReply } =
|
|
33
|
+
const { editTelegramMessage, sendTelegramReply, sendTelegramRichReply } =
|
|
34
|
+
await import("./send.js");
|
|
34
35
|
const { telegramTransport } = await import("./transport.js");
|
|
35
36
|
|
|
36
37
|
const approval: ApprovalUIMetadata = {
|
|
@@ -191,15 +192,15 @@ describe("telegramTransport.deliver routing", () => {
|
|
|
191
192
|
} as ChannelReplyPayload;
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
test("routes to the rich send when
|
|
195
|
-
await telegramTransport.deliver(ctx, payload({
|
|
195
|
+
test("routes to the rich send when a rich render is asked for", async () => {
|
|
196
|
+
await telegramTransport.deliver(ctx, payload({ renderRichly: true }));
|
|
196
197
|
|
|
197
198
|
expect(callsTo("sendRichMessage")).toHaveLength(1);
|
|
198
199
|
expect(callsTo("sendMessage")).toHaveLength(0);
|
|
199
200
|
});
|
|
200
201
|
|
|
201
|
-
test("stays on the plain send when
|
|
202
|
-
await telegramTransport.deliver(ctx, payload({
|
|
202
|
+
test("stays on the plain send when it is not", async () => {
|
|
203
|
+
await telegramTransport.deliver(ctx, payload({ renderRichly: false }));
|
|
203
204
|
|
|
204
205
|
expect(callsTo("sendRichMessage")).toHaveLength(0);
|
|
205
206
|
expect(callsTo("sendMessage")).toHaveLength(1);
|
|
@@ -208,7 +209,7 @@ describe("telegramTransport.deliver routing", () => {
|
|
|
208
209
|
test("forwards approval metadata through the rich path", async () => {
|
|
209
210
|
await telegramTransport.deliver(
|
|
210
211
|
ctx,
|
|
211
|
-
payload({
|
|
212
|
+
payload({ renderRichly: true, approval } as Partial<ChannelReplyPayload>),
|
|
212
213
|
);
|
|
213
214
|
|
|
214
215
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendRichMessage", {
|
|
@@ -238,7 +239,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
test("plain replies target the topic from the callback threadId param", async () => {
|
|
241
|
-
await telegramTransport.deliver(topicCtx, payload({
|
|
242
|
+
await telegramTransport.deliver(topicCtx, payload({ renderRichly: false }));
|
|
242
243
|
|
|
243
244
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendMessage", {
|
|
244
245
|
chat_id: "123",
|
|
@@ -248,7 +249,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
248
249
|
});
|
|
249
250
|
|
|
250
251
|
test("rich replies target the topic", async () => {
|
|
251
|
-
await telegramTransport.deliver(topicCtx, payload({
|
|
252
|
+
await telegramTransport.deliver(topicCtx, payload({ renderRichly: true }));
|
|
252
253
|
|
|
253
254
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendRichMessage", {
|
|
254
255
|
chat_id: "123",
|
|
@@ -262,7 +263,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
262
263
|
throw new TelegramNonRetryableError("rejected", "rejected");
|
|
263
264
|
});
|
|
264
265
|
|
|
265
|
-
await telegramTransport.deliver(topicCtx, payload({
|
|
266
|
+
await telegramTransport.deliver(topicCtx, payload({ renderRichly: true }));
|
|
266
267
|
|
|
267
268
|
expect(callTelegramBotApiMock).toHaveBeenNthCalledWith(2, "sendMessage", {
|
|
268
269
|
chat_id: "123",
|
|
@@ -272,7 +273,10 @@ describe("telegramTransport topic targeting", () => {
|
|
|
272
273
|
});
|
|
273
274
|
|
|
274
275
|
test("typing indicators target the topic", async () => {
|
|
275
|
-
await telegramTransport.
|
|
276
|
+
await telegramTransport.setActivity!(topicCtx, {
|
|
277
|
+
chatId: "123",
|
|
278
|
+
phase: "thinking",
|
|
279
|
+
});
|
|
276
280
|
|
|
277
281
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendChatAction", {
|
|
278
282
|
chat_id: "123",
|
|
@@ -281,14 +285,32 @@ describe("telegramTransport topic targeting", () => {
|
|
|
281
285
|
});
|
|
282
286
|
});
|
|
283
287
|
|
|
288
|
+
// Telegram's chat action expires on its own, so a phase that is not running
|
|
289
|
+
// has nothing to say. Asserting the call count rather than the absence of a
|
|
290
|
+
// "stop" call is what catches a clearing request being invented later.
|
|
291
|
+
test.each(["idle", "awaiting_confirmation"] as const)(
|
|
292
|
+
"says nothing to Telegram for the %s phase",
|
|
293
|
+
async (phase) => {
|
|
294
|
+
await telegramTransport.setActivity!(topicCtx, {
|
|
295
|
+
chatId: "123",
|
|
296
|
+
phase,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(callTelegramBotApiMock).not.toHaveBeenCalled();
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
284
303
|
test("a callback URL without threadId keeps sends thread-less", async () => {
|
|
285
304
|
const bareCtx: CallbackContext = {
|
|
286
305
|
callbackUrl: "/deliver/telegram",
|
|
287
306
|
params: {},
|
|
288
307
|
};
|
|
289
308
|
|
|
290
|
-
await telegramTransport.deliver(bareCtx, payload({
|
|
291
|
-
await telegramTransport.
|
|
309
|
+
await telegramTransport.deliver(bareCtx, payload({ renderRichly: false }));
|
|
310
|
+
await telegramTransport.setActivity!(bareCtx, {
|
|
311
|
+
chatId: "123",
|
|
312
|
+
phase: "thinking",
|
|
313
|
+
});
|
|
292
314
|
|
|
293
315
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendMessage", {
|
|
294
316
|
chat_id: "123",
|
|
@@ -300,3 +322,78 @@ describe("telegramTransport topic targeting", () => {
|
|
|
300
322
|
});
|
|
301
323
|
});
|
|
302
324
|
});
|
|
325
|
+
|
|
326
|
+
describe("editTelegramMessage", () => {
|
|
327
|
+
const ctx = { callbackUrl: "http://gw/deliver/telegram", params: {} };
|
|
328
|
+
|
|
329
|
+
const sendMessageCalls = () =>
|
|
330
|
+
callTelegramBotApiMock.mock.calls.filter(
|
|
331
|
+
(call) => call[0] === "sendMessage",
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
test("edits in place and never posts a new message", async () => {
|
|
335
|
+
await telegramTransport.edit!(ctx as CallbackContext, {
|
|
336
|
+
chatId: "123",
|
|
337
|
+
messageId: "456",
|
|
338
|
+
text: "revised",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(callTelegramBotApiMock).toHaveBeenCalledTimes(1);
|
|
342
|
+
const [method, body] = callTelegramBotApiMock.mock.calls[0]!;
|
|
343
|
+
expect(method).toBe("editMessageText");
|
|
344
|
+
// Telegram wants a numeric message id, where the capability carries the
|
|
345
|
+
// channel's id as a string.
|
|
346
|
+
expect(body).toEqual({
|
|
347
|
+
chat_id: "123",
|
|
348
|
+
message_id: 456,
|
|
349
|
+
text: "revised",
|
|
350
|
+
reply_markup: { inline_keyboard: [] },
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("clears the inline keyboard, so a settled message keeps no buttons", async () => {
|
|
355
|
+
await telegramTransport.edit!(ctx as CallbackContext, {
|
|
356
|
+
chatId: "123",
|
|
357
|
+
messageId: "456",
|
|
358
|
+
text: "\u2713 Approved",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const [, body] = callTelegramBotApiMock.mock.calls[0]!;
|
|
362
|
+
// Omitting reply_markup leaves an existing keyboard in place, which would
|
|
363
|
+
// leave live Approve and Reject buttons under text saying the request is
|
|
364
|
+
// already decided. The field has to be sent, and sent empty.
|
|
365
|
+
expect((body as Record<string, unknown>).reply_markup).toEqual({
|
|
366
|
+
inline_keyboard: [],
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("treats an unchanged message as already done", async () => {
|
|
371
|
+
callTelegramBotApiMock.mockImplementationOnce(async () => {
|
|
372
|
+
throw new TelegramNonRetryableError(
|
|
373
|
+
"Bad Request",
|
|
374
|
+
"Bad Request: message is not modified",
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// The edit asked for a state the message is already in, which is the
|
|
379
|
+
// request satisfied rather than refused.
|
|
380
|
+
await expect(
|
|
381
|
+
editTelegramMessage("123", "456", "same"),
|
|
382
|
+
).resolves.toBeUndefined();
|
|
383
|
+
expect(sendMessageCalls()).toHaveLength(0);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("throws on any other rejection rather than posting a replacement", async () => {
|
|
387
|
+
callTelegramBotApiMock.mockImplementationOnce(async () => {
|
|
388
|
+
throw new TelegramNonRetryableError(
|
|
389
|
+
"Bad Request",
|
|
390
|
+
"Bad Request: message to edit not found",
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Posting instead would leave the original beside a duplicate, so the
|
|
395
|
+
// failure has to reach the caller.
|
|
396
|
+
await expect(editTelegramMessage("123", "456", "gone")).rejects.toThrow();
|
|
397
|
+
expect(sendMessageCalls()).toHaveLength(0);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -137,6 +137,49 @@ export interface TelegramSendResult {
|
|
|
137
137
|
* Send a Telegram text reply, splitting long messages and optionally
|
|
138
138
|
* attaching inline keyboard buttons for approval prompts.
|
|
139
139
|
*/
|
|
140
|
+
/**
|
|
141
|
+
* Replace a Telegram message in place.
|
|
142
|
+
*
|
|
143
|
+
* Telegram rejects an edit whose text already matches the message. That is the
|
|
144
|
+
* request having been satisfied rather than a failure, so it resolves. Every
|
|
145
|
+
* other rejection throws: an edit that quietly became a new message would
|
|
146
|
+
* leave the original sitting beside it, which reads as answering twice.
|
|
147
|
+
*
|
|
148
|
+
* Unlike a send, this cannot split long text across messages, because an edit
|
|
149
|
+
* addresses exactly one. Telegram rejects text past its limit, and that
|
|
150
|
+
* rejection reaches the caller rather than being papered over.
|
|
151
|
+
*
|
|
152
|
+
* The empty `reply_markup` is load-bearing. `editMessageText` leaves an
|
|
153
|
+
* existing inline keyboard alone when the field is omitted, so a message
|
|
154
|
+
* revised to read as settled would keep its live buttons beside that text.
|
|
155
|
+
* Every caller here edits a message into a settled state, and the approval
|
|
156
|
+
* interception path says so outright, so the keyboard goes with the revision.
|
|
157
|
+
*/
|
|
158
|
+
export async function editTelegramMessage(
|
|
159
|
+
chatId: string,
|
|
160
|
+
messageId: string,
|
|
161
|
+
text: string,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
try {
|
|
164
|
+
await callTelegramBotApi<TelegramMessage>("editMessageText", {
|
|
165
|
+
chat_id: chatId,
|
|
166
|
+
message_id: Number(messageId),
|
|
167
|
+
text,
|
|
168
|
+
reply_markup: { inline_keyboard: [] },
|
|
169
|
+
});
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (
|
|
172
|
+
err instanceof TelegramNonRetryableError &&
|
|
173
|
+
err.description?.includes("message is not modified")
|
|
174
|
+
) {
|
|
175
|
+
log.debug({ chatId, messageId }, "Telegram edit already applied");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
log.debug({ chatId, messageId }, "Telegram message edited");
|
|
181
|
+
}
|
|
182
|
+
|
|
140
183
|
export async function sendTelegramReply(
|
|
141
184
|
chatId: string,
|
|
142
185
|
text: string,
|
|
@@ -5,8 +5,10 @@ import type {
|
|
|
5
5
|
CallbackContext,
|
|
6
6
|
ChannelTransport,
|
|
7
7
|
} from "../channel-transport.js";
|
|
8
|
+
import { isBusyActivityPhase } from "../channel-transport.js";
|
|
8
9
|
import type { TelegramSendOptions } from "./send.js";
|
|
9
10
|
import {
|
|
11
|
+
editTelegramMessage,
|
|
10
12
|
sendTelegramAttachments,
|
|
11
13
|
sendTelegramReply,
|
|
12
14
|
sendTelegramRichReply,
|
|
@@ -27,16 +29,18 @@ function threadOptions(ctx: CallbackContext): TelegramSendOptions | undefined {
|
|
|
27
29
|
export const telegramTransport: ChannelTransport = {
|
|
28
30
|
channel: "telegram",
|
|
29
31
|
|
|
32
|
+
// Telegram clears a chat action after about five seconds.
|
|
33
|
+
activityRefreshMs: 4_000,
|
|
34
|
+
|
|
30
35
|
async deliver(ctx, payload) {
|
|
31
36
|
const { chatId, text, attachments, approval } = payload;
|
|
32
37
|
const opts = threadOptions(ctx);
|
|
33
38
|
|
|
34
39
|
if (text) {
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
if (payload.useBlocks) {
|
|
40
|
+
// Telegram answers a rich render by forwarding markdown to
|
|
41
|
+
// `sendRichMessage`, degrading to plain text otherwise and on any
|
|
42
|
+
// rich-send rejection.
|
|
43
|
+
if (payload.renderRichly) {
|
|
40
44
|
await sendTelegramRichReply(chatId, text, approval, opts);
|
|
41
45
|
} else {
|
|
42
46
|
await sendTelegramReply(chatId, text, approval, opts);
|
|
@@ -67,9 +71,22 @@ export const telegramTransport: ChannelTransport = {
|
|
|
67
71
|
return { ok: true };
|
|
68
72
|
},
|
|
69
73
|
|
|
70
|
-
async
|
|
71
|
-
await
|
|
72
|
-
|
|
74
|
+
async edit(_ctx, target) {
|
|
75
|
+
await editTelegramMessage(target.chatId, target.messageId, target.text);
|
|
76
|
+
return { ok: true };
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async setActivity(ctx, target) {
|
|
80
|
+
// Telegram's chat action expires by itself after a few seconds, so a phase
|
|
81
|
+
// that is not running needs no clearing call.
|
|
82
|
+
if (!isBusyActivityPhase(target.phase)) {
|
|
83
|
+
return { ok: true };
|
|
84
|
+
}
|
|
85
|
+
await sendTelegramTypingIndicator(target.chatId, threadOptions(ctx));
|
|
86
|
+
log.debug(
|
|
87
|
+
{ chatId: target.chatId, phase: target.phase },
|
|
88
|
+
"Telegram typing indicator delivered (direct)",
|
|
89
|
+
);
|
|
73
90
|
return { ok: true };
|
|
74
91
|
},
|
|
75
92
|
};
|
|
@@ -200,6 +200,17 @@ function makeMacOriginatedMessage(): MessageRow {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
function makeWindowsOriginatedMessage(): MessageRow {
|
|
204
|
+
return makeMessage({
|
|
205
|
+
metadata: JSON.stringify({
|
|
206
|
+
userMessageChannel: "vellum",
|
|
207
|
+
userMessageInterface: "web",
|
|
208
|
+
client: { os: "windows" },
|
|
209
|
+
clientOsFromRequest: true,
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
/**
|
|
204
215
|
* A turn opened from a plain browser tab, as opposed to the Electron desktop
|
|
205
216
|
* renderer sharing the same web bundle: `client.os` is `"web"` only when
|
|
@@ -347,7 +358,7 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
347
358
|
desktopAttended = true;
|
|
348
359
|
});
|
|
349
360
|
|
|
350
|
-
test("marks the signal source-active while a
|
|
361
|
+
test("marks the signal source-active while a desktop reports itself attended", async () => {
|
|
351
362
|
await run();
|
|
352
363
|
|
|
353
364
|
expect(emitCalls).toHaveLength(1);
|
|
@@ -357,7 +368,16 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
357
368
|
expect(desktopPresenceArgs).toEqual([[]]);
|
|
358
369
|
});
|
|
359
370
|
|
|
360
|
-
test("
|
|
371
|
+
test("marks a Windows-originated signal source-active while attended", async () => {
|
|
372
|
+
initiatingRow = makeWindowsOriginatedMessage();
|
|
373
|
+
|
|
374
|
+
await run();
|
|
375
|
+
|
|
376
|
+
expect(emitCalls).toHaveLength(1);
|
|
377
|
+
expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("leaves the signal live when no desktop is attended", async () => {
|
|
361
381
|
desktopAttended = false;
|
|
362
382
|
|
|
363
383
|
await run();
|
|
@@ -386,10 +406,13 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
386
406
|
expect(warnCalls).toHaveLength(1);
|
|
387
407
|
});
|
|
388
408
|
|
|
389
|
-
// An attended
|
|
390
|
-
// can send from the phone minutes after last touching the keyboard,
|
|
409
|
+
// An attended desktop only speaks for a turn that desktop itself opened.
|
|
410
|
+
// The user can send from the phone minutes after last touching the keyboard,
|
|
391
411
|
// is exactly the reply this producer exists to push.
|
|
392
|
-
const
|
|
412
|
+
const NON_DESKTOP_ORIGIN_CASES: Array<{
|
|
413
|
+
name: string;
|
|
414
|
+
metadata: unknown;
|
|
415
|
+
}> = [
|
|
393
416
|
{
|
|
394
417
|
name: "the iOS app",
|
|
395
418
|
metadata: { client: { os: "ios" }, clientOsFromRequest: true },
|
|
@@ -412,7 +435,7 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
412
435
|
// `macos` the conversation's live client state kept from an earlier
|
|
413
436
|
// desktop send. Without the marker that OS names an earlier turn, not
|
|
414
437
|
// this one, and the push the user is waiting for on their phone would
|
|
415
|
-
// be dropped by the attended
|
|
438
|
+
// be dropped by the attended desktop.
|
|
416
439
|
{
|
|
417
440
|
name: "a row whose macOS OS was inherited, not reported",
|
|
418
441
|
metadata: { userMessageInterface: "web", client: { os: "macos" } },
|
|
@@ -424,7 +447,7 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
424
447
|
},
|
|
425
448
|
];
|
|
426
449
|
|
|
427
|
-
for (const { name, metadata } of
|
|
450
|
+
for (const { name, metadata } of NON_DESKTOP_ORIGIN_CASES) {
|
|
428
451
|
test(`leaves the signal live for a turn opened from ${name}`, async () => {
|
|
429
452
|
initiatingRow = makeMessage({ metadata: JSON.stringify(metadata) });
|
|
430
453
|
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
* plain text with typed-command instructions.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
editTelegramMessage,
|
|
13
|
+
sendTelegramReply,
|
|
14
|
+
} from "../../messaging/providers/telegram-bot/send.js";
|
|
12
15
|
import { ConfigError } from "../../util/errors.js";
|
|
13
16
|
import { getLogger } from "../../util/logger.js";
|
|
14
17
|
import type {
|
|
15
18
|
ChannelAdapter,
|
|
16
19
|
ChannelDeliveryPayload,
|
|
17
20
|
ChannelDestination,
|
|
21
|
+
ChannelUpdateContext,
|
|
22
|
+
ChannelUpdatePayload,
|
|
18
23
|
DeliveryResult,
|
|
19
24
|
NotificationChannel,
|
|
20
25
|
} from "../types.js";
|
|
@@ -97,4 +102,46 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
97
102
|
return { success: false, error: message };
|
|
98
103
|
}
|
|
99
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Replace a delivered notification in place, so a card that has been
|
|
108
|
+
* answered or withdrawn stops reading as still waiting.
|
|
109
|
+
*
|
|
110
|
+
* `editTelegramMessage` clears the card's inline keyboard as part of the
|
|
111
|
+
* revision, so a card rewritten to read as settled cannot keep live Approve
|
|
112
|
+
* and Reject buttons beside that text.
|
|
113
|
+
*/
|
|
114
|
+
async update(
|
|
115
|
+
delivery: ChannelUpdateContext,
|
|
116
|
+
patch: ChannelUpdatePayload,
|
|
117
|
+
): Promise<DeliveryResult> {
|
|
118
|
+
if (!delivery.messageId) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
error:
|
|
122
|
+
"missing_message_id: this delivery has no captured Telegram message id",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const text = patch.body?.trim() || patch.title?.trim();
|
|
126
|
+
if (!text) {
|
|
127
|
+
return { success: false, error: "no body or title supplied for update" };
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await editTelegramMessage(delivery.destination, delivery.messageId, text);
|
|
131
|
+
log.info(
|
|
132
|
+
{ chatId: delivery.destination, messageId: delivery.messageId },
|
|
133
|
+
"Telegram notification updated",
|
|
134
|
+
);
|
|
135
|
+
// An edit keeps the message it addressed, so the delivery row's id
|
|
136
|
+
// still identifies the card.
|
|
137
|
+
return { success: true, messageId: delivery.messageId };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
log.error(
|
|
141
|
+
{ err, chatId: delivery.destination, messageId: delivery.messageId },
|
|
142
|
+
"Failed to update Telegram notification",
|
|
143
|
+
);
|
|
144
|
+
return { success: false, error: message };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
100
147
|
}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
parseMessageMetadata,
|
|
24
24
|
} from "../persistence/conversation-crud.js";
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
isDesktopOriginatedUserMessage,
|
|
27
27
|
isReplyPushIneligibleUserMessage,
|
|
28
28
|
resolveConversationKind,
|
|
29
29
|
} from "../persistence/conversation-types.js";
|
|
@@ -122,7 +122,7 @@ function readSuppressionMarkers(
|
|
|
122
122
|
*
|
|
123
123
|
* No `actorPrincipalId`: the platform delivers this push to the assistant
|
|
124
124
|
* owner's device tokens, and a pod has exactly one owner, so any attended
|
|
125
|
-
*
|
|
125
|
+
* desktop client is that owner's.
|
|
126
126
|
*/
|
|
127
127
|
function readDesktopAttended(rlog: pino.Logger): boolean {
|
|
128
128
|
try {
|
|
@@ -246,12 +246,12 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
246
246
|
);
|
|
247
247
|
|
|
248
248
|
// Read as close to the emit as possible: nothing short-circuits on it.
|
|
249
|
-
// Presence only speaks for a turn the
|
|
250
|
-
// OS evidence. A turn sent from the phone still needs its push while the
|
|
251
|
-
//
|
|
249
|
+
// Presence only speaks for a turn the desktop itself opened, on that row's
|
|
250
|
+
// own OS evidence. A turn sent from the phone still needs its push while the
|
|
251
|
+
// desktop sits idle within the attendance window.
|
|
252
252
|
const desktopAttended =
|
|
253
253
|
isAssistantFeatureFlagEnabled(DESKTOP_PRESENCE_FLAG) &&
|
|
254
|
-
|
|
254
|
+
isDesktopOriginatedUserMessage(initiatingMetadata) &&
|
|
255
255
|
readDesktopAttended(rlog);
|
|
256
256
|
|
|
257
257
|
// Conversation-scoped web presence applies regardless of which device
|
|
@@ -276,7 +276,7 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
276
276
|
// opting into v2.
|
|
277
277
|
urgency: "medium",
|
|
278
278
|
isAsyncBackground: false,
|
|
279
|
-
// Read weakly, as "at the surface this landed on": the attended
|
|
279
|
+
// Read weakly, as "at the surface this landed on": the attended desktop
|
|
280
280
|
// or focused web tab that opened the turn renders the reply in-app,
|
|
281
281
|
// so a redundant push would only duplicate what's already on screen.
|
|
282
282
|
visibleInSourceNow: desktopAttended || webFocused,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates richer seed content for notification conversations than the concise
|
|
5
5
|
* title/body used in native notification popups. Verbosity adapts to the
|
|
6
|
-
* delivery surface:
|
|
6
|
+
* delivery surface: Vellum desktop gets flowing prose, Telegram gets compact.
|
|
7
7
|
*
|
|
8
8
|
* Composes from `copy.title/body` rather than hardcoded English templates
|
|
9
9
|
* so LLM-localized copy is preserved for non-English users.
|
|
@@ -25,7 +25,12 @@ const CHANNEL_DEFAULT_INTERFACE: Record<string, InterfaceId> = {
|
|
|
25
25
|
telegram: "telegram",
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const RICH_INTERFACES = new Set<InterfaceId>([
|
|
28
|
+
const RICH_INTERFACES = new Set<InterfaceId>([
|
|
29
|
+
"macos",
|
|
30
|
+
"windows",
|
|
31
|
+
"ios",
|
|
32
|
+
"web",
|
|
33
|
+
]);
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
36
|
* Resolve verbosity level from delivery channel + optional interface hint.
|
|
@@ -358,6 +358,43 @@ describe("BYOOAuthConnection", () => {
|
|
|
358
358
|
expect(headers.get("Content-Type")).toBe("application/json");
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
+
test("sends a string body verbatim under the caller's Content-Type", async () => {
|
|
362
|
+
await setupCredential("google");
|
|
363
|
+
const conn = createConnection();
|
|
364
|
+
const multipart =
|
|
365
|
+
"--boundary\r\nContent-Type: application/json\r\n\r\n{}\r\n--boundary--\r\n";
|
|
366
|
+
|
|
367
|
+
await conn.request({
|
|
368
|
+
method: "POST",
|
|
369
|
+
path: "/upload/drive/v3/files",
|
|
370
|
+
headers: { "Content-Type": "multipart/related; boundary=boundary" },
|
|
371
|
+
body: multipart,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
375
|
+
expect((init as RequestInit).body).toBe(multipart);
|
|
376
|
+
const headers = (init as RequestInit).headers as Headers;
|
|
377
|
+
expect(headers.get("Content-Type")).toBe(
|
|
378
|
+
"multipart/related; boundary=boundary",
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("does not force a JSON Content-Type onto a string body", async () => {
|
|
383
|
+
await setupCredential("google");
|
|
384
|
+
const conn = createConnection();
|
|
385
|
+
|
|
386
|
+
await conn.request({
|
|
387
|
+
method: "POST",
|
|
388
|
+
path: "/upload",
|
|
389
|
+
body: "name=sheet&kind=grid",
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
393
|
+
expect((init as RequestInit).body).toBe("name=sheet&kind=grid");
|
|
394
|
+
const headers = (init as RequestInit).headers as Headers;
|
|
395
|
+
expect(headers.has("Content-Type")).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
361
398
|
test("retries once on 401 response", async () => {
|
|
362
399
|
await setupCredential("google");
|
|
363
400
|
const conn = createConnection();
|
|
@@ -420,6 +457,32 @@ describe("BYOOAuthConnection", () => {
|
|
|
420
457
|
expect(result.body).toBe("plain text response");
|
|
421
458
|
});
|
|
422
459
|
|
|
460
|
+
test("preserves non-ASCII binary bytes for media downloads", async () => {
|
|
461
|
+
await setupCredential("google");
|
|
462
|
+
const conn = createConnection();
|
|
463
|
+
const binary = Buffer.from([
|
|
464
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe, 0x00,
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
globalThis.fetch = mock(() =>
|
|
468
|
+
Promise.resolve(
|
|
469
|
+
new Response(binary, {
|
|
470
|
+
status: 200,
|
|
471
|
+
headers: { "content-type": "application/octet-stream" },
|
|
472
|
+
}),
|
|
473
|
+
),
|
|
474
|
+
) as unknown as typeof fetch;
|
|
475
|
+
|
|
476
|
+
const result = await conn.request({
|
|
477
|
+
method: "GET",
|
|
478
|
+
path: "/drive/v3/files/file-123?alt=media",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(result.status).toBe(200);
|
|
482
|
+
expect(Buffer.isBuffer(result.body)).toBe(true);
|
|
483
|
+
expect(Buffer.from(result.body as Uint8Array).equals(binary)).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
|
|
423
486
|
test("returns response headers", async () => {
|
|
424
487
|
await setupCredential("google");
|
|
425
488
|
const conn = createConnection();
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
OAuthConnectionRequest,
|
|
15
15
|
OAuthConnectionResponse,
|
|
16
16
|
} from "./connection.js";
|
|
17
|
+
import { decodeOAuthResponseBytes } from "./connection.js";
|
|
17
18
|
|
|
18
19
|
const log = getLogger("byo-oauth-connection");
|
|
19
20
|
|
|
@@ -80,10 +81,17 @@ export class BYOOAuthConnection implements OAuthConnection {
|
|
|
80
81
|
"Making authenticated request",
|
|
81
82
|
);
|
|
82
83
|
|
|
84
|
+
// A string body is already in its wire form (multipart, XML,
|
|
85
|
+
// form-encoded, or pre-serialized JSON) and travels verbatim under
|
|
86
|
+
// the caller's own Content-Type. Objects and arrays are serialized
|
|
87
|
+
// as JSON and get the JSON Content-Type by default.
|
|
88
|
+
const hasBody = req.body !== undefined && req.body !== null;
|
|
89
|
+
const rawBody = typeof req.body === "string" ? req.body : undefined;
|
|
90
|
+
|
|
83
91
|
// Use the Headers API for case-insensitive merging. Set defaults
|
|
84
92
|
// first so caller-supplied headers (in any casing) override them.
|
|
85
93
|
const headers = new Headers();
|
|
86
|
-
if (
|
|
94
|
+
if (hasBody && rawBody === undefined) {
|
|
87
95
|
headers.set("Content-Type", "application/json");
|
|
88
96
|
}
|
|
89
97
|
if (req.headers) {
|
|
@@ -98,7 +106,7 @@ export class BYOOAuthConnection implements OAuthConnection {
|
|
|
98
106
|
const resp = await fetch(fullUrl, {
|
|
99
107
|
method: req.method,
|
|
100
108
|
headers,
|
|
101
|
-
body:
|
|
109
|
+
body: hasBody ? (rawBody ?? JSON.stringify(req.body)) : undefined,
|
|
102
110
|
signal: req.signal
|
|
103
111
|
? AbortSignal.any([
|
|
104
112
|
req.signal,
|
|
@@ -150,17 +158,10 @@ async function buildResponse(resp: Response): Promise<OAuthConnectionResponse> {
|
|
|
150
158
|
headers[key] = value;
|
|
151
159
|
});
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
body = text;
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
body = null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return { status: resp.status, headers, body };
|
|
161
|
+
const raw = Buffer.from(await resp.arrayBuffer());
|
|
162
|
+
return {
|
|
163
|
+
status: resp.status,
|
|
164
|
+
headers,
|
|
165
|
+
body: decodeOAuthResponseBytes(raw, headers["content-type"] ?? ""),
|
|
166
|
+
};
|
|
166
167
|
}
|