@vellumai/vellum-gateway 0.5.1 → 0.5.3
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/package.json +1 -1
- package/src/__tests__/config.test.ts +1 -0
- package/src/__tests__/route-schema-guard.test.ts +3 -0
- package/src/__tests__/schema.test.ts +16 -0
- package/src/config.ts +2 -1
- package/src/feature-flag-registry.json +24 -0
- package/src/http/router.ts +8 -2
- package/src/http/routes/oauth-apps-proxy.ts +129 -0
- package/src/index.ts +42 -0
- package/src/runtime/client.ts +49 -26
- package/src/schema.ts +331 -0
- package/src/telegram/send.ts +4 -2
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ describe("config: hardcoded defaults", () => {
|
|
|
11
11
|
expect(config.maxWebhookPayloadBytes).toBe(1024 * 1024);
|
|
12
12
|
expect(config.maxAttachmentBytes).toEqual({
|
|
13
13
|
telegram: 20 * 1024 * 1024,
|
|
14
|
+
telegramOutbound: 50 * 1024 * 1024,
|
|
14
15
|
slack: 100 * 1024 * 1024,
|
|
15
16
|
whatsapp: 16 * 1024 * 1024,
|
|
16
17
|
default: 100 * 1024 * 1024,
|
|
@@ -111,6 +111,9 @@ function regexToOpenApiPath(escaped: string): string | null {
|
|
|
111
111
|
return `{param${paramIndex}}`;
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// Strip optional trailing slash (`/?`) — common in route regexes
|
|
115
|
+
path = path.replace(/\/\?$/, "");
|
|
116
|
+
|
|
114
117
|
// If there are remaining regex constructs we can't convert, skip
|
|
115
118
|
if (/[\\()\[\].*+?{}|^$]/.test(path.replace(/\{param\d+\}/g, ""))) {
|
|
116
119
|
return null;
|
|
@@ -64,6 +64,11 @@ describe("/schema route", () => {
|
|
|
64
64
|
expect(body.paths["/v1/integrations/telegram/config"]).toBeDefined();
|
|
65
65
|
expect(body.paths["/v1/integrations/telegram/commands"]).toBeDefined();
|
|
66
66
|
expect(body.paths["/v1/integrations/telegram/setup"]).toBeDefined();
|
|
67
|
+
expect(body.paths["/v1/oauth/apps"]).toBeDefined();
|
|
68
|
+
expect(body.paths["/v1/oauth/apps/{appId}"]).toBeDefined();
|
|
69
|
+
expect(body.paths["/v1/oauth/apps/{appId}/connections"]).toBeDefined();
|
|
70
|
+
expect(body.paths["/v1/oauth/connections/{connectionId}"]).toBeDefined();
|
|
71
|
+
expect(body.paths["/v1/oauth/apps/{appId}/connect"]).toBeDefined();
|
|
67
72
|
expect(body.paths["/v1/contacts"]).toBeDefined();
|
|
68
73
|
expect(body.paths["/v1/contacts/merge"]).toBeDefined();
|
|
69
74
|
expect(body.paths["/v1/contact-channels/{contactChannelId}"]).toBeDefined();
|
|
@@ -131,6 +136,17 @@ describe("buildSchema()", () => {
|
|
|
131
136
|
expect(schemaNames).toContain("TelegramDocument");
|
|
132
137
|
expect(schemaNames).toContain("TelegramDeliverRequest");
|
|
133
138
|
expect(schemaNames).toContain("RuntimeAttachmentMeta");
|
|
139
|
+
|
|
140
|
+
const oauthConnection = components.schemas.OAuthConnectionSummary as {
|
|
141
|
+
properties?: Record<string, unknown>;
|
|
142
|
+
};
|
|
143
|
+
expect(oauthConnection.properties?.granted_scopes).toEqual({
|
|
144
|
+
type: "array",
|
|
145
|
+
items: { type: "string" },
|
|
146
|
+
});
|
|
147
|
+
expect(oauthConnection.properties?.has_refresh_token).toEqual({
|
|
148
|
+
type: "boolean",
|
|
149
|
+
});
|
|
134
150
|
});
|
|
135
151
|
|
|
136
152
|
test("returns a JSON-serializable object", () => {
|
package/src/config.ts
CHANGED
|
@@ -137,7 +137,8 @@ export function loadConfig(): GatewayConfig {
|
|
|
137
137
|
gatewayInternalBaseUrl,
|
|
138
138
|
logFile,
|
|
139
139
|
maxAttachmentBytes: {
|
|
140
|
-
telegram: 20 * 1024 * 1024, // Telegram Bot API getFile limit
|
|
140
|
+
telegram: 20 * 1024 * 1024, // Telegram Bot API getFile (download) limit
|
|
141
|
+
telegramOutbound: 50 * 1024 * 1024, // Telegram Bot API sendDocument (upload) limit
|
|
141
142
|
slack: 100 * 1024 * 1024, // Slack standard plan
|
|
142
143
|
whatsapp: 16 * 1024 * 1024, // WhatsApp Business API limit
|
|
143
144
|
default: 100 * 1024 * 1024, // Fallback; capped by runtime MAX_UPLOAD_BYTES (100 MB)
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
"description": "Show the Contacts tab in Settings for viewing and managing contacts",
|
|
26
26
|
"defaultEnabled": true
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
"id": "custom-inference-provider",
|
|
30
|
+
"scope": "macos",
|
|
31
|
+
"key": "custom_inference_provider_enabled",
|
|
32
|
+
"label": "Custom Inference Provider",
|
|
33
|
+
"description": "Allow selecting a specific LLM provider and model for inference in Your Own mode",
|
|
34
|
+
"defaultEnabled": false
|
|
35
|
+
},
|
|
28
36
|
{
|
|
29
37
|
"id": "email-channel",
|
|
30
38
|
"scope": "assistant",
|
|
@@ -249,6 +257,14 @@
|
|
|
249
257
|
"description": "Show the Google OAuth service card in Models & Services settings",
|
|
250
258
|
"defaultEnabled": false
|
|
251
259
|
},
|
|
260
|
+
{
|
|
261
|
+
"id": "settings-embedding-provider",
|
|
262
|
+
"scope": "assistant",
|
|
263
|
+
"key": "feature_flags.settings-embedding-provider.enabled",
|
|
264
|
+
"label": "Embedding Provider Settings",
|
|
265
|
+
"description": "Show the Embedding service card in Models & Services settings",
|
|
266
|
+
"defaultEnabled": false
|
|
267
|
+
},
|
|
252
268
|
{
|
|
253
269
|
"id": "quick-input",
|
|
254
270
|
"scope": "macos",
|
|
@@ -264,6 +280,14 @@
|
|
|
264
280
|
"label": "Expand Completed Steps",
|
|
265
281
|
"description": "Auto-expand completed tool call step groups instead of showing them collapsed",
|
|
266
282
|
"defaultEnabled": false
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"id": "inline-skill-commands",
|
|
286
|
+
"scope": "assistant",
|
|
287
|
+
"key": "feature_flags.inline-skill-commands.enabled",
|
|
288
|
+
"label": "Inline Skill Command Expansion",
|
|
289
|
+
"description": "Enable secure inline skill command expansion via !`command` syntax, with version-pinned approval and sandboxed execution at skill load time",
|
|
290
|
+
"defaultEnabled": true
|
|
267
291
|
}
|
|
268
292
|
]
|
|
269
293
|
}
|
package/src/http/router.ts
CHANGED
|
@@ -160,11 +160,17 @@ function matchRoute(
|
|
|
160
160
|
if (route.method && route.method !== method) return null;
|
|
161
161
|
|
|
162
162
|
if (typeof route.path === "string") {
|
|
163
|
-
|
|
163
|
+
// Normalize trailing slashes so "/v1/foo/" matches "/v1/foo"
|
|
164
|
+
const normalized =
|
|
165
|
+
pathname.length > 1 && pathname.endsWith("/")
|
|
166
|
+
? pathname.slice(0, -1)
|
|
167
|
+
: pathname;
|
|
168
|
+
if (normalized !== route.path) return null;
|
|
164
169
|
return { params: [] };
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
// Regex path
|
|
172
|
+
// Regex path — use original pathname since regex routes may
|
|
173
|
+
// explicitly include trailing slashes in their patterns.
|
|
168
174
|
const match = pathname.match(route.path);
|
|
169
175
|
if (!match) return null;
|
|
170
176
|
return { params: match.slice(1) };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway proxy endpoints for OAuth app and connection management routes.
|
|
3
|
+
*
|
|
4
|
+
* These routes remain available even when the broad runtime proxy is
|
|
5
|
+
* disabled, so skills and clients can use gateway URLs exclusively.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mintServiceToken } from "../../auth/token-exchange.js";
|
|
9
|
+
import type { GatewayConfig } from "../../config.js";
|
|
10
|
+
import { fetchImpl } from "../../fetch.js";
|
|
11
|
+
import { getLogger } from "../../logger.js";
|
|
12
|
+
import { stripHopByHop } from "../../util/strip-hop-by-hop.js";
|
|
13
|
+
|
|
14
|
+
const log = getLogger("oauth-apps-proxy");
|
|
15
|
+
|
|
16
|
+
export function createOAuthAppsProxyHandler(config: GatewayConfig) {
|
|
17
|
+
async function proxyToRuntime(
|
|
18
|
+
req: Request,
|
|
19
|
+
upstreamPath: string,
|
|
20
|
+
upstreamSearch: string,
|
|
21
|
+
): Promise<Response> {
|
|
22
|
+
const start = performance.now();
|
|
23
|
+
const upstream = `${config.assistantRuntimeBaseUrl}${upstreamPath}${upstreamSearch}`;
|
|
24
|
+
|
|
25
|
+
const reqHeaders = stripHopByHop(new Headers(req.headers));
|
|
26
|
+
reqHeaders.delete("host");
|
|
27
|
+
reqHeaders.delete("authorization");
|
|
28
|
+
|
|
29
|
+
reqHeaders.set("authorization", `Bearer ${mintServiceToken()}`);
|
|
30
|
+
|
|
31
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
32
|
+
const bodyBuffer = hasBody ? await req.arrayBuffer() : null;
|
|
33
|
+
if (bodyBuffer !== null) {
|
|
34
|
+
reqHeaders.set("content-length", String(bodyBuffer.byteLength));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeoutId = setTimeout(() => {
|
|
39
|
+
controller.abort(
|
|
40
|
+
new DOMException(
|
|
41
|
+
"The operation was aborted due to timeout",
|
|
42
|
+
"TimeoutError",
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}, config.runtimeTimeoutMs);
|
|
46
|
+
|
|
47
|
+
let response: Response;
|
|
48
|
+
try {
|
|
49
|
+
response = await fetchImpl(upstream, {
|
|
50
|
+
method: req.method,
|
|
51
|
+
headers: reqHeaders,
|
|
52
|
+
body: bodyBuffer,
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
const duration = Math.round(performance.now() - start);
|
|
59
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
60
|
+
log.error(
|
|
61
|
+
{ path: upstreamPath, duration },
|
|
62
|
+
"OAuth apps proxy upstream timed out",
|
|
63
|
+
);
|
|
64
|
+
return Response.json({ error: "Gateway Timeout" }, { status: 504 });
|
|
65
|
+
}
|
|
66
|
+
log.error(
|
|
67
|
+
{ err, path: upstreamPath, duration },
|
|
68
|
+
"OAuth apps proxy upstream connection failed",
|
|
69
|
+
);
|
|
70
|
+
return Response.json({ error: "Bad Gateway" }, { status: 502 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const resHeaders = stripHopByHop(new Headers(response.headers));
|
|
74
|
+
const duration = Math.round(performance.now() - start);
|
|
75
|
+
|
|
76
|
+
if (response.status >= 400) {
|
|
77
|
+
const body = await response.text();
|
|
78
|
+
log.warn(
|
|
79
|
+
{ path: upstreamPath, status: response.status, duration },
|
|
80
|
+
"OAuth apps proxy upstream error",
|
|
81
|
+
);
|
|
82
|
+
return new Response(body, {
|
|
83
|
+
status: response.status,
|
|
84
|
+
headers: resHeaders,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log.info(
|
|
89
|
+
{ path: upstreamPath, status: response.status, duration },
|
|
90
|
+
"OAuth apps proxy completed",
|
|
91
|
+
);
|
|
92
|
+
return new Response(response.body, {
|
|
93
|
+
status: response.status,
|
|
94
|
+
headers: resHeaders,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
async handleListApps(req: Request): Promise<Response> {
|
|
100
|
+
return proxyToRuntime(req, "/v1/oauth/apps", new URL(req.url).search);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async handleCreateApp(req: Request): Promise<Response> {
|
|
104
|
+
return proxyToRuntime(req, "/v1/oauth/apps", "");
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async handleDeleteApp(req: Request, appId: string): Promise<Response> {
|
|
108
|
+
return proxyToRuntime(req, `/v1/oauth/apps/${appId}`, "");
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async handleListConnections(
|
|
112
|
+
req: Request,
|
|
113
|
+
appId: string,
|
|
114
|
+
): Promise<Response> {
|
|
115
|
+
return proxyToRuntime(req, `/v1/oauth/apps/${appId}/connections`, "");
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async handleDeleteConnection(
|
|
119
|
+
req: Request,
|
|
120
|
+
connectionId: string,
|
|
121
|
+
): Promise<Response> {
|
|
122
|
+
return proxyToRuntime(req, `/v1/oauth/connections/${connectionId}`, "");
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async handleConnect(req: Request, appId: string): Promise<Response> {
|
|
126
|
+
return proxyToRuntime(req, `/v1/oauth/apps/${appId}/connect`, "");
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { createTelegramControlPlaneProxyHandler } from "./http/routes/telegram-c
|
|
|
50
50
|
import { createContactsControlPlaneProxyHandler } from "./http/routes/contacts-control-plane-proxy.js";
|
|
51
51
|
import { createTwilioControlPlaneProxyHandler } from "./http/routes/twilio-control-plane-proxy.js";
|
|
52
52
|
import { createSlackControlPlaneProxyHandler } from "./http/routes/slack-control-plane-proxy.js";
|
|
53
|
+
import { createOAuthAppsProxyHandler } from "./http/routes/oauth-apps-proxy.js";
|
|
53
54
|
import { createChannelReadinessProxyHandler } from "./http/routes/channel-readiness-proxy.js";
|
|
54
55
|
import { createRuntimeHealthProxyHandler } from "./http/routes/runtime-health-proxy.js";
|
|
55
56
|
import { createBrainGraphProxyHandler } from "./http/routes/brain-graph-proxy.js";
|
|
@@ -266,6 +267,7 @@ async function main() {
|
|
|
266
267
|
createContactsControlPlaneProxyHandler(config);
|
|
267
268
|
const twilioControlPlaneProxy = createTwilioControlPlaneProxyHandler(config);
|
|
268
269
|
const slackControlPlaneProxy = createSlackControlPlaneProxyHandler(config);
|
|
270
|
+
const oauthAppsProxy = createOAuthAppsProxyHandler(config);
|
|
269
271
|
const channelReadinessProxy = createChannelReadinessProxyHandler(config);
|
|
270
272
|
const runtimeHealthProxy = createRuntimeHealthProxyHandler(config);
|
|
271
273
|
const brainGraphProxy = createBrainGraphProxyHandler(config);
|
|
@@ -649,6 +651,46 @@ async function main() {
|
|
|
649
651
|
handler: (req) => slackControlPlaneProxy.handleShareToSlack(req),
|
|
650
652
|
},
|
|
651
653
|
|
|
654
|
+
// ── OAuth apps ──
|
|
655
|
+
{
|
|
656
|
+
path: "/v1/oauth/apps",
|
|
657
|
+
method: "GET",
|
|
658
|
+
auth: "edge",
|
|
659
|
+
handler: (req) => oauthAppsProxy.handleListApps(req),
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
path: "/v1/oauth/apps",
|
|
663
|
+
method: "POST",
|
|
664
|
+
auth: "edge",
|
|
665
|
+
handler: (req) => oauthAppsProxy.handleCreateApp(req),
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
path: /^\/v1\/oauth\/apps\/([^/]+)\/?$/,
|
|
669
|
+
method: "DELETE",
|
|
670
|
+
auth: "edge",
|
|
671
|
+
handler: (req, params) => oauthAppsProxy.handleDeleteApp(req, params[0]),
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
path: /^\/v1\/oauth\/apps\/([^/]+)\/connections\/?$/,
|
|
675
|
+
method: "GET",
|
|
676
|
+
auth: "edge",
|
|
677
|
+
handler: (req, params) =>
|
|
678
|
+
oauthAppsProxy.handleListConnections(req, params[0]),
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
path: /^\/v1\/oauth\/connections\/([^/]+)\/?$/,
|
|
682
|
+
method: "DELETE",
|
|
683
|
+
auth: "edge",
|
|
684
|
+
handler: (req, params) =>
|
|
685
|
+
oauthAppsProxy.handleDeleteConnection(req, params[0]),
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
path: /^\/v1\/oauth\/apps\/([^/]+)\/connect\/?$/,
|
|
689
|
+
method: "POST",
|
|
690
|
+
auth: "edge",
|
|
691
|
+
handler: (req, params) => oauthAppsProxy.handleConnect(req, params[0]),
|
|
692
|
+
},
|
|
693
|
+
|
|
652
694
|
// ── Channel readiness ──
|
|
653
695
|
{
|
|
654
696
|
path: "/v1/channels/readiness",
|
package/src/runtime/client.ts
CHANGED
|
@@ -310,47 +310,61 @@ export type UploadAttachmentResponse = {
|
|
|
310
310
|
id: string;
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Internal helper that fetches raw attachment content without interacting
|
|
315
|
+
* with the circuit breaker. Used by downloadAttachment's hydration path
|
|
316
|
+
* which already owns the breaker lifecycle for the compound operation.
|
|
317
|
+
*/
|
|
318
|
+
async function fetchAttachmentContentRaw(
|
|
314
319
|
config: GatewayConfig,
|
|
315
320
|
attachmentId: string,
|
|
316
321
|
): Promise<Buffer> {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
const url = `${
|
|
323
|
+
config.assistantRuntimeBaseUrl
|
|
324
|
+
}/v1/attachments/${encodeURIComponent(attachmentId)}/content`;
|
|
320
325
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
signal: AbortSignal.timeout(config.runtimeTimeoutMs),
|
|
327
|
-
});
|
|
328
|
-
} catch (err) {
|
|
329
|
-
cbOnFailure();
|
|
330
|
-
throw err;
|
|
331
|
-
}
|
|
326
|
+
const response = await fetchImpl(url, {
|
|
327
|
+
method: "GET",
|
|
328
|
+
headers: runtimeServiceHeaders(config),
|
|
329
|
+
signal: AbortSignal.timeout(config.runtimeTimeoutMs),
|
|
330
|
+
});
|
|
332
331
|
|
|
333
332
|
if (!response.ok) {
|
|
334
333
|
const body = await response.text();
|
|
335
|
-
if (response.status >= 500) cbOnFailure();
|
|
336
|
-
else cbOnSuccess();
|
|
337
334
|
throw new Error(
|
|
338
335
|
`Attachment content download failed (${response.status}): ${body}`,
|
|
339
336
|
);
|
|
340
337
|
}
|
|
341
338
|
|
|
342
|
-
cbOnSuccess();
|
|
343
339
|
const arrayBuffer = await response.arrayBuffer();
|
|
344
340
|
return Buffer.from(arrayBuffer);
|
|
345
341
|
}
|
|
346
342
|
|
|
343
|
+
export async function downloadAttachmentContent(
|
|
344
|
+
config: GatewayConfig,
|
|
345
|
+
attachmentId: string,
|
|
346
|
+
): Promise<Buffer> {
|
|
347
|
+
cbBeforeRequest();
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const buffer = await fetchAttachmentContentRaw(config, attachmentId);
|
|
351
|
+
cbOnSuccess();
|
|
352
|
+
return buffer;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
cbOnFailure();
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
347
359
|
export async function downloadAttachment(
|
|
348
360
|
config: GatewayConfig,
|
|
349
361
|
attachmentId: string,
|
|
350
362
|
): Promise<HydratedAttachmentPayload> {
|
|
351
363
|
cbBeforeRequest();
|
|
352
364
|
|
|
353
|
-
const url = `${
|
|
365
|
+
const url = `${
|
|
366
|
+
config.assistantRuntimeBaseUrl
|
|
367
|
+
}/v1/attachments/${encodeURIComponent(attachmentId)}`;
|
|
354
368
|
|
|
355
369
|
let response: Response;
|
|
356
370
|
try {
|
|
@@ -375,15 +389,24 @@ export async function downloadAttachment(
|
|
|
375
389
|
|
|
376
390
|
// Transparently hydrate file-backed attachments: fetch the binary content
|
|
377
391
|
// from the dedicated /content endpoint and inline it as base64.
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
if (payload.fileBacked &&
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
// We use the raw helper (no nested circuit breaker) so the compound
|
|
393
|
+
// metadata+content operation is treated as a single breaker unit.
|
|
394
|
+
// If content fetch fails, cbOnFailure() fires for the whole operation.
|
|
395
|
+
if (payload.fileBacked && payload.data == null) {
|
|
396
|
+
try {
|
|
397
|
+
const contentBuffer = await fetchAttachmentContentRaw(
|
|
398
|
+
config,
|
|
399
|
+
attachmentId,
|
|
400
|
+
);
|
|
401
|
+
payload.data = contentBuffer.toString("base64");
|
|
402
|
+
} catch (err) {
|
|
403
|
+
cbOnFailure();
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
384
406
|
}
|
|
385
407
|
|
|
386
|
-
|
|
408
|
+
// Use == null to allow empty string (valid base64 for zero-byte attachments)
|
|
409
|
+
if (payload.data == null) {
|
|
387
410
|
throw new Error(`Attachment ${attachmentId} has no data after hydration`);
|
|
388
411
|
}
|
|
389
412
|
|
package/src/schema.ts
CHANGED
|
@@ -1615,6 +1615,226 @@ export function buildSchema(): Record<string, unknown> {
|
|
|
1615
1615
|
},
|
|
1616
1616
|
},
|
|
1617
1617
|
},
|
|
1618
|
+
"/v1/oauth/apps": {
|
|
1619
|
+
get: {
|
|
1620
|
+
summary: "List OAuth apps",
|
|
1621
|
+
description:
|
|
1622
|
+
"Authenticated gateway endpoint that lists configured OAuth apps for a provider by proxying to the assistant runtime.",
|
|
1623
|
+
operationId: "oauthAppsList",
|
|
1624
|
+
parameters: [
|
|
1625
|
+
{
|
|
1626
|
+
name: "provider_key",
|
|
1627
|
+
in: "query",
|
|
1628
|
+
required: true,
|
|
1629
|
+
schema: { type: "string" },
|
|
1630
|
+
description:
|
|
1631
|
+
"OAuth provider key to filter by, for example `integration:google`.",
|
|
1632
|
+
},
|
|
1633
|
+
],
|
|
1634
|
+
security: [{ BearerAuth: [] }],
|
|
1635
|
+
responses: {
|
|
1636
|
+
"200": {
|
|
1637
|
+
description: "OAuth apps returned",
|
|
1638
|
+
content: {
|
|
1639
|
+
"application/json": {
|
|
1640
|
+
schema: {
|
|
1641
|
+
$ref: "#/components/schemas/OAuthAppListResponse",
|
|
1642
|
+
},
|
|
1643
|
+
},
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
"400": { description: "Missing or invalid provider_key query parameter" },
|
|
1647
|
+
"401": {
|
|
1648
|
+
description: "Unauthorized — missing or invalid bearer token",
|
|
1649
|
+
},
|
|
1650
|
+
"503": { description: "Bearer token not configured" },
|
|
1651
|
+
"502": { description: "Failed to reach assistant runtime" },
|
|
1652
|
+
"504": { description: "Assistant runtime request timed out" },
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1655
|
+
post: {
|
|
1656
|
+
summary: "Create OAuth app",
|
|
1657
|
+
description:
|
|
1658
|
+
"Authenticated gateway endpoint that creates or updates a user-managed OAuth app by proxying to the assistant runtime.",
|
|
1659
|
+
operationId: "oauthAppsCreate",
|
|
1660
|
+
security: [{ BearerAuth: [] }],
|
|
1661
|
+
requestBody: {
|
|
1662
|
+
required: true,
|
|
1663
|
+
content: {
|
|
1664
|
+
"application/json": {
|
|
1665
|
+
schema: { $ref: "#/components/schemas/OAuthAppCreateRequest" },
|
|
1666
|
+
},
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1669
|
+
responses: {
|
|
1670
|
+
"201": {
|
|
1671
|
+
description: "OAuth app created",
|
|
1672
|
+
content: {
|
|
1673
|
+
"application/json": {
|
|
1674
|
+
schema: {
|
|
1675
|
+
$ref: "#/components/schemas/OAuthAppCreateResponse",
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
"400": { description: "Invalid request payload" },
|
|
1681
|
+
"401": {
|
|
1682
|
+
description: "Unauthorized — missing or invalid bearer token",
|
|
1683
|
+
},
|
|
1684
|
+
"404": { description: "OAuth provider not found" },
|
|
1685
|
+
"503": { description: "Bearer token not configured" },
|
|
1686
|
+
"502": { description: "Failed to reach assistant runtime" },
|
|
1687
|
+
"504": { description: "Assistant runtime request timed out" },
|
|
1688
|
+
},
|
|
1689
|
+
},
|
|
1690
|
+
},
|
|
1691
|
+
"/v1/oauth/apps/{appId}": {
|
|
1692
|
+
delete: {
|
|
1693
|
+
summary: "Delete OAuth app",
|
|
1694
|
+
description:
|
|
1695
|
+
"Authenticated gateway endpoint that deletes a user-managed OAuth app and disconnects its linked accounts by proxying to the assistant runtime.",
|
|
1696
|
+
operationId: "oauthAppsDelete",
|
|
1697
|
+
parameters: [
|
|
1698
|
+
{
|
|
1699
|
+
name: "appId",
|
|
1700
|
+
in: "path",
|
|
1701
|
+
required: true,
|
|
1702
|
+
schema: { type: "string" },
|
|
1703
|
+
},
|
|
1704
|
+
],
|
|
1705
|
+
security: [{ BearerAuth: [] }],
|
|
1706
|
+
responses: {
|
|
1707
|
+
"200": {
|
|
1708
|
+
description: "OAuth app deleted",
|
|
1709
|
+
content: {
|
|
1710
|
+
"application/json": {
|
|
1711
|
+
schema: { $ref: "#/components/schemas/OkResponse" },
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
"401": {
|
|
1716
|
+
description: "Unauthorized — missing or invalid bearer token",
|
|
1717
|
+
},
|
|
1718
|
+
"404": { description: "OAuth app not found" },
|
|
1719
|
+
"503": { description: "Bearer token not configured" },
|
|
1720
|
+
"502": { description: "Failed to reach assistant runtime" },
|
|
1721
|
+
"504": { description: "Assistant runtime request timed out" },
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
"/v1/oauth/apps/{appId}/connections": {
|
|
1726
|
+
get: {
|
|
1727
|
+
summary: "List OAuth app connections",
|
|
1728
|
+
description:
|
|
1729
|
+
"Authenticated gateway endpoint that lists linked accounts for a specific OAuth app by proxying to the assistant runtime.",
|
|
1730
|
+
operationId: "oauthAppConnectionsList",
|
|
1731
|
+
parameters: [
|
|
1732
|
+
{
|
|
1733
|
+
name: "appId",
|
|
1734
|
+
in: "path",
|
|
1735
|
+
required: true,
|
|
1736
|
+
schema: { type: "string" },
|
|
1737
|
+
},
|
|
1738
|
+
],
|
|
1739
|
+
security: [{ BearerAuth: [] }],
|
|
1740
|
+
responses: {
|
|
1741
|
+
"200": {
|
|
1742
|
+
description: "OAuth app connections returned",
|
|
1743
|
+
content: {
|
|
1744
|
+
"application/json": {
|
|
1745
|
+
schema: {
|
|
1746
|
+
$ref: "#/components/schemas/OAuthConnectionListResponse",
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1751
|
+
"401": {
|
|
1752
|
+
description: "Unauthorized — missing or invalid bearer token",
|
|
1753
|
+
},
|
|
1754
|
+
"404": { description: "OAuth app not found" },
|
|
1755
|
+
"503": { description: "Bearer token not configured" },
|
|
1756
|
+
"502": { description: "Failed to reach assistant runtime" },
|
|
1757
|
+
"504": { description: "Assistant runtime request timed out" },
|
|
1758
|
+
},
|
|
1759
|
+
},
|
|
1760
|
+
},
|
|
1761
|
+
"/v1/oauth/connections/{connectionId}": {
|
|
1762
|
+
delete: {
|
|
1763
|
+
summary: "Delete OAuth connection",
|
|
1764
|
+
description:
|
|
1765
|
+
"Authenticated gateway endpoint that disconnects a linked OAuth account by proxying to the assistant runtime.",
|
|
1766
|
+
operationId: "oauthConnectionDelete",
|
|
1767
|
+
parameters: [
|
|
1768
|
+
{
|
|
1769
|
+
name: "connectionId",
|
|
1770
|
+
in: "path",
|
|
1771
|
+
required: true,
|
|
1772
|
+
schema: { type: "string" },
|
|
1773
|
+
},
|
|
1774
|
+
],
|
|
1775
|
+
security: [{ BearerAuth: [] }],
|
|
1776
|
+
responses: {
|
|
1777
|
+
"200": {
|
|
1778
|
+
description: "OAuth connection deleted",
|
|
1779
|
+
content: {
|
|
1780
|
+
"application/json": {
|
|
1781
|
+
schema: { $ref: "#/components/schemas/OkResponse" },
|
|
1782
|
+
},
|
|
1783
|
+
},
|
|
1784
|
+
},
|
|
1785
|
+
"401": {
|
|
1786
|
+
description: "Unauthorized — missing or invalid bearer token",
|
|
1787
|
+
},
|
|
1788
|
+
"404": { description: "OAuth connection not found" },
|
|
1789
|
+
"503": { description: "Bearer token not configured" },
|
|
1790
|
+
"502": { description: "Failed to reach assistant runtime" },
|
|
1791
|
+
"504": { description: "Assistant runtime request timed out" },
|
|
1792
|
+
},
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
1795
|
+
"/v1/oauth/apps/{appId}/connect": {
|
|
1796
|
+
post: {
|
|
1797
|
+
summary: "Start OAuth app connect flow",
|
|
1798
|
+
description:
|
|
1799
|
+
"Authenticated gateway endpoint that starts an OAuth authorization flow for a specific app by proxying to the assistant runtime.",
|
|
1800
|
+
operationId: "oauthAppConnect",
|
|
1801
|
+
parameters: [
|
|
1802
|
+
{
|
|
1803
|
+
name: "appId",
|
|
1804
|
+
in: "path",
|
|
1805
|
+
required: true,
|
|
1806
|
+
schema: { type: "string" },
|
|
1807
|
+
},
|
|
1808
|
+
],
|
|
1809
|
+
security: [{ BearerAuth: [] }],
|
|
1810
|
+
requestBody: {
|
|
1811
|
+
required: false,
|
|
1812
|
+
content: {
|
|
1813
|
+
"application/json": {
|
|
1814
|
+
schema: { $ref: "#/components/schemas/OAuthConnectRequest" },
|
|
1815
|
+
},
|
|
1816
|
+
},
|
|
1817
|
+
},
|
|
1818
|
+
responses: {
|
|
1819
|
+
"200": {
|
|
1820
|
+
description: "OAuth connect flow started",
|
|
1821
|
+
content: {
|
|
1822
|
+
"application/json": {
|
|
1823
|
+
schema: { $ref: "#/components/schemas/OAuthConnectResponse" },
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1826
|
+
},
|
|
1827
|
+
"401": {
|
|
1828
|
+
description: "Unauthorized — missing or invalid bearer token",
|
|
1829
|
+
},
|
|
1830
|
+
"404": { description: "OAuth app not found" },
|
|
1831
|
+
"500": { description: "Failed to start OAuth flow" },
|
|
1832
|
+
"503": { description: "Bearer token not configured" },
|
|
1833
|
+
"502": { description: "Failed to reach assistant runtime" },
|
|
1834
|
+
"504": { description: "Assistant runtime request timed out" },
|
|
1835
|
+
},
|
|
1836
|
+
},
|
|
1837
|
+
},
|
|
1618
1838
|
"/v1/channels/readiness": {
|
|
1619
1839
|
get: {
|
|
1620
1840
|
summary: "Get channel readiness",
|
|
@@ -2263,6 +2483,117 @@ export function buildSchema(): Record<string, unknown> {
|
|
|
2263
2483
|
error: { type: "string" },
|
|
2264
2484
|
},
|
|
2265
2485
|
},
|
|
2486
|
+
OkResponse: {
|
|
2487
|
+
type: "object",
|
|
2488
|
+
required: ["ok"],
|
|
2489
|
+
properties: {
|
|
2490
|
+
ok: { type: "boolean" },
|
|
2491
|
+
},
|
|
2492
|
+
},
|
|
2493
|
+
OAuthAppSummary: {
|
|
2494
|
+
type: "object",
|
|
2495
|
+
required: [
|
|
2496
|
+
"id",
|
|
2497
|
+
"provider_key",
|
|
2498
|
+
"client_id",
|
|
2499
|
+
"created_at",
|
|
2500
|
+
"updated_at",
|
|
2501
|
+
],
|
|
2502
|
+
properties: {
|
|
2503
|
+
id: { type: "string" },
|
|
2504
|
+
provider_key: { type: "string" },
|
|
2505
|
+
client_id: { type: "string" },
|
|
2506
|
+
created_at: { type: "integer" },
|
|
2507
|
+
updated_at: { type: "integer" },
|
|
2508
|
+
},
|
|
2509
|
+
},
|
|
2510
|
+
OAuthAppListResponse: {
|
|
2511
|
+
type: "object",
|
|
2512
|
+
required: ["apps"],
|
|
2513
|
+
properties: {
|
|
2514
|
+
apps: {
|
|
2515
|
+
type: "array",
|
|
2516
|
+
items: { $ref: "#/components/schemas/OAuthAppSummary" },
|
|
2517
|
+
},
|
|
2518
|
+
},
|
|
2519
|
+
},
|
|
2520
|
+
OAuthAppCreateRequest: {
|
|
2521
|
+
type: "object",
|
|
2522
|
+
required: ["provider_key", "client_id", "client_secret"],
|
|
2523
|
+
properties: {
|
|
2524
|
+
provider_key: { type: "string" },
|
|
2525
|
+
client_id: { type: "string" },
|
|
2526
|
+
client_secret: { type: "string" },
|
|
2527
|
+
},
|
|
2528
|
+
},
|
|
2529
|
+
OAuthAppCreateResponse: {
|
|
2530
|
+
type: "object",
|
|
2531
|
+
required: ["app"],
|
|
2532
|
+
properties: {
|
|
2533
|
+
app: { $ref: "#/components/schemas/OAuthAppSummary" },
|
|
2534
|
+
},
|
|
2535
|
+
},
|
|
2536
|
+
OAuthConnectionSummary: {
|
|
2537
|
+
type: "object",
|
|
2538
|
+
required: [
|
|
2539
|
+
"id",
|
|
2540
|
+
"provider_key",
|
|
2541
|
+
"account_info",
|
|
2542
|
+
"granted_scopes",
|
|
2543
|
+
"status",
|
|
2544
|
+
"has_refresh_token",
|
|
2545
|
+
"expires_at",
|
|
2546
|
+
"created_at",
|
|
2547
|
+
"updated_at",
|
|
2548
|
+
],
|
|
2549
|
+
properties: {
|
|
2550
|
+
id: { type: "string" },
|
|
2551
|
+
provider_key: { type: "string" },
|
|
2552
|
+
account_info: { type: ["string", "null"] },
|
|
2553
|
+
granted_scopes: {
|
|
2554
|
+
type: "array",
|
|
2555
|
+
items: { type: "string" },
|
|
2556
|
+
},
|
|
2557
|
+
status: { type: "string" },
|
|
2558
|
+
has_refresh_token: { type: "boolean" },
|
|
2559
|
+
expires_at: { type: ["integer", "null"] },
|
|
2560
|
+
created_at: { type: "integer" },
|
|
2561
|
+
updated_at: { type: "integer" },
|
|
2562
|
+
},
|
|
2563
|
+
},
|
|
2564
|
+
OAuthConnectionListResponse: {
|
|
2565
|
+
type: "object",
|
|
2566
|
+
required: ["connections"],
|
|
2567
|
+
properties: {
|
|
2568
|
+
connections: {
|
|
2569
|
+
type: "array",
|
|
2570
|
+
items: { $ref: "#/components/schemas/OAuthConnectionSummary" },
|
|
2571
|
+
},
|
|
2572
|
+
},
|
|
2573
|
+
},
|
|
2574
|
+
OAuthConnectRequest: {
|
|
2575
|
+
type: "object",
|
|
2576
|
+
properties: {
|
|
2577
|
+
scopes: {
|
|
2578
|
+
type: "array",
|
|
2579
|
+
items: { type: "string" },
|
|
2580
|
+
},
|
|
2581
|
+
},
|
|
2582
|
+
},
|
|
2583
|
+
OAuthConnectDeferredResponse: {
|
|
2584
|
+
type: "object",
|
|
2585
|
+
required: ["auth_url", "state"],
|
|
2586
|
+
properties: {
|
|
2587
|
+
auth_url: { type: "string" },
|
|
2588
|
+
state: { type: "string" },
|
|
2589
|
+
},
|
|
2590
|
+
},
|
|
2591
|
+
OAuthConnectResponse: {
|
|
2592
|
+
oneOf: [
|
|
2593
|
+
{ $ref: "#/components/schemas/OAuthConnectDeferredResponse" },
|
|
2594
|
+
{ $ref: "#/components/schemas/OkResponse" },
|
|
2595
|
+
],
|
|
2596
|
+
},
|
|
2266
2597
|
TelegramOk: {
|
|
2267
2598
|
type: "object",
|
|
2268
2599
|
required: ["ok"],
|
package/src/telegram/send.ts
CHANGED
|
@@ -82,10 +82,12 @@ export async function sendTelegramAttachments(
|
|
|
82
82
|
|
|
83
83
|
for (const meta of attachments) {
|
|
84
84
|
// When size is known upfront, skip oversized attachments before downloading.
|
|
85
|
+
// Use the outbound limit (sendDocument supports 50 MB) rather than the
|
|
86
|
+
// inbound getFile limit (20 MB).
|
|
85
87
|
if (
|
|
86
88
|
meta.sizeBytes !== undefined &&
|
|
87
89
|
meta.sizeBytes >
|
|
88
|
-
(config.maxAttachmentBytes.
|
|
90
|
+
(config.maxAttachmentBytes.telegramOutbound ??
|
|
89
91
|
config.maxAttachmentBytes.default)
|
|
90
92
|
) {
|
|
91
93
|
log.warn(
|
|
@@ -111,7 +113,7 @@ export async function sendTelegramAttachments(
|
|
|
111
113
|
// Check size after hydration for ID-only payloads where size was unknown.
|
|
112
114
|
if (
|
|
113
115
|
sizeBytes >
|
|
114
|
-
(config.maxAttachmentBytes.
|
|
116
|
+
(config.maxAttachmentBytes.telegramOutbound ??
|
|
115
117
|
config.maxAttachmentBytes.default)
|
|
116
118
|
) {
|
|
117
119
|
log.warn(
|