@slates/provider-handler 1.0.0-rc.21 → 1.0.0-rc.24
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/dist/index.cjs +77 -5
- package/dist/index.module.js +80 -6
- package/package.json +4 -3
- package/src/attachments.test.ts +171 -0
- package/src/index.ts +112 -4
- package/src/spec.ts +1 -2
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
36
36
|
var import_error4 = require("@lowerdeck/error");
|
|
37
37
|
var import_proto = require("@slates/proto");
|
|
38
38
|
var import_provider2 = require("@slates/provider");
|
|
39
|
+
var import_p_queue = __toESM(require("p-queue"), 1);
|
|
39
40
|
|
|
40
41
|
// src/spec.ts
|
|
41
42
|
var import_error2 = require("@lowerdeck/error");
|
|
@@ -94,8 +95,7 @@ var mapAuthMethod = (slate, m) => ({
|
|
|
94
95
|
scopes: "scopes" in m ? m.scopes.map((s) => ({
|
|
95
96
|
id: s.scope,
|
|
96
97
|
title: s.title,
|
|
97
|
-
description: s.description
|
|
98
|
-
defaultChecked: s.defaultChecked
|
|
98
|
+
description: s.description
|
|
99
99
|
})) : void 0,
|
|
100
100
|
inputSchema: toJsonSchema(m.inputSchema ?? import_zod.default.object({})),
|
|
101
101
|
outputSchema: toJsonSchema(slate.spec.auth.outputSchema),
|
|
@@ -253,6 +253,36 @@ var serializeWebhookHttpResponse = async (response) => {
|
|
|
253
253
|
};
|
|
254
254
|
|
|
255
255
|
// src/index.ts
|
|
256
|
+
var DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 1e8;
|
|
257
|
+
var routeAttachmentsThroughDirectUpload = async (attachments, live) => {
|
|
258
|
+
if (!attachments || attachments.length === 0 || !live) return attachments;
|
|
259
|
+
let contentEntries = attachments.map((attachment, index) => ({ attachment, index })).filter((entry) => entry.attachment.content.type === "content");
|
|
260
|
+
if (contentEntries.length === 0) return attachments;
|
|
261
|
+
let queue = new import_p_queue.default({ concurrency: 10 });
|
|
262
|
+
let replacements = /* @__PURE__ */ new Map();
|
|
263
|
+
await Promise.all(
|
|
264
|
+
contentEntries.map(
|
|
265
|
+
(entry) => queue.add(async () => {
|
|
266
|
+
try {
|
|
267
|
+
let body = entry.attachment.content.type === "content" ? entry.attachment.content.encoding === "base64" ? Buffer.from(entry.attachment.content.content, "base64") : Buffer.from(entry.attachment.content.content, "utf-8") : Buffer.alloc(0);
|
|
268
|
+
replacements.set(
|
|
269
|
+
entry.index,
|
|
270
|
+
await (0, import_provider2.uploadAttachmentDirect)({
|
|
271
|
+
live,
|
|
272
|
+
mimeType: entry.attachment.mimeType,
|
|
273
|
+
body
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
} catch {
|
|
277
|
+
replacements.set(entry.index, null);
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
return attachments.map(
|
|
283
|
+
(attachment, index) => replacements.has(index) ? replacements.get(index) : attachment
|
|
284
|
+
).filter((a) => a != null);
|
|
285
|
+
};
|
|
256
286
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
257
287
|
var getObjectKeyCount = (value) => isRecord(value) ? Object.keys(value).length : void 0;
|
|
258
288
|
var DOWNLOAD_ATTACHMENT_URL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -316,6 +346,19 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
|
|
|
316
346
|
let auth = new State(null);
|
|
317
347
|
let config = new State(null);
|
|
318
348
|
let session = new State(null);
|
|
349
|
+
let hubCapabilities = new State(null);
|
|
350
|
+
let liveInvocation = new State(
|
|
351
|
+
null
|
|
352
|
+
);
|
|
353
|
+
let getLiveInvocation = () => {
|
|
354
|
+
let directUpload = hubCapabilities.get()?.attachments?.directUpload;
|
|
355
|
+
let live = liveInvocation.get();
|
|
356
|
+
if (!directUpload?.enabled || !live) return null;
|
|
357
|
+
return {
|
|
358
|
+
...live,
|
|
359
|
+
maxAttachmentSizeBytes: directUpload.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES
|
|
360
|
+
};
|
|
361
|
+
};
|
|
319
362
|
let logger = new import_provider2.SlateLogger(listeners);
|
|
320
363
|
let providerTrace = {
|
|
321
364
|
providerId: slate.spec.key,
|
|
@@ -442,6 +485,15 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
|
|
|
442
485
|
state: params.state
|
|
443
486
|
});
|
|
444
487
|
});
|
|
488
|
+
manager.onNotification("slates/hub.capabilities.set", async ({ params }) => {
|
|
489
|
+
hubCapabilities.set(params.capabilities);
|
|
490
|
+
});
|
|
491
|
+
manager.onNotification("slates/hub.live_invocation.set", async ({ params }) => {
|
|
492
|
+
liveInvocation.set({
|
|
493
|
+
token: params.token,
|
|
494
|
+
baseUrl: params.baseUrl
|
|
495
|
+
});
|
|
496
|
+
});
|
|
445
497
|
manager.onRequest("slates/config.changed", async ({ params }) => {
|
|
446
498
|
getContextBasic();
|
|
447
499
|
let newConfig = validate(
|
|
@@ -906,19 +958,39 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
|
|
|
906
958
|
},
|
|
907
959
|
() => (0, import_provider2.runWithContext)(context, () => action.handleInvocation(context))
|
|
908
960
|
);
|
|
961
|
+
let contextAttachments = await context._finalizeAttachments();
|
|
962
|
+
let merged = mergeAttachments(
|
|
963
|
+
[...res.attachments ?? [], ...contextAttachments],
|
|
964
|
+
res.output
|
|
965
|
+
);
|
|
966
|
+
let redacted = (0, import_provider2.redactUrlAttachmentSecrets)(
|
|
967
|
+
merged,
|
|
968
|
+
context._getAuthConfigForRedaction()
|
|
969
|
+
);
|
|
970
|
+
let finalAttachments = await routeAttachmentsThroughDirectUpload(
|
|
971
|
+
redacted,
|
|
972
|
+
getLiveInvocation()
|
|
973
|
+
);
|
|
909
974
|
return withRequestTraces(context, {
|
|
910
975
|
output: res.output,
|
|
911
976
|
message: res.message,
|
|
912
|
-
attachments:
|
|
977
|
+
attachments: finalAttachments
|
|
913
978
|
});
|
|
914
979
|
};
|
|
915
980
|
if (action.isPublic) {
|
|
916
981
|
getContextBasic();
|
|
917
|
-
return invoke(new import_provider2.SlatePublicContext(input, slate.spec, logger));
|
|
982
|
+
return invoke(new import_provider2.SlatePublicContext(input, slate.spec, logger, getLiveInvocation()));
|
|
918
983
|
}
|
|
919
984
|
let ctx = getContextFull();
|
|
920
985
|
return invoke(
|
|
921
|
-
new import_provider2.SlateContext(
|
|
986
|
+
new import_provider2.SlateContext(
|
|
987
|
+
ctx.config,
|
|
988
|
+
input,
|
|
989
|
+
ctx.auth?.output,
|
|
990
|
+
slate.spec,
|
|
991
|
+
logger,
|
|
992
|
+
getLiveInvocation()
|
|
993
|
+
)
|
|
922
994
|
);
|
|
923
995
|
});
|
|
924
996
|
manager.onRequest("slates/action.trigger.map_event", async ({ params }) => {
|
package/dist/index.module.js
CHANGED
|
@@ -5,11 +5,14 @@ import {
|
|
|
5
5
|
SLATES_PROTOCOL_VERSION
|
|
6
6
|
} from "@slates/proto";
|
|
7
7
|
import {
|
|
8
|
+
redactUrlAttachmentSecrets,
|
|
8
9
|
runWithContext,
|
|
9
10
|
SlateContext,
|
|
10
11
|
SlateLogger,
|
|
11
|
-
SlatePublicContext
|
|
12
|
+
SlatePublicContext,
|
|
13
|
+
uploadAttachmentDirect
|
|
12
14
|
} from "@slates/provider";
|
|
15
|
+
import PQueue from "p-queue";
|
|
13
16
|
|
|
14
17
|
// src/spec.ts
|
|
15
18
|
import { badRequestError, notFoundError, ServiceError as ServiceError2 } from "@lowerdeck/error";
|
|
@@ -70,8 +73,7 @@ var mapAuthMethod = (slate, m) => ({
|
|
|
70
73
|
scopes: "scopes" in m ? m.scopes.map((s) => ({
|
|
71
74
|
id: s.scope,
|
|
72
75
|
title: s.title,
|
|
73
|
-
description: s.description
|
|
74
|
-
defaultChecked: s.defaultChecked
|
|
76
|
+
description: s.description
|
|
75
77
|
})) : void 0,
|
|
76
78
|
inputSchema: toJsonSchema(m.inputSchema ?? z.object({})),
|
|
77
79
|
outputSchema: toJsonSchema(slate.spec.auth.outputSchema),
|
|
@@ -229,6 +231,36 @@ var serializeWebhookHttpResponse = async (response) => {
|
|
|
229
231
|
};
|
|
230
232
|
|
|
231
233
|
// src/index.ts
|
|
234
|
+
var DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 1e8;
|
|
235
|
+
var routeAttachmentsThroughDirectUpload = async (attachments, live) => {
|
|
236
|
+
if (!attachments || attachments.length === 0 || !live) return attachments;
|
|
237
|
+
let contentEntries = attachments.map((attachment, index) => ({ attachment, index })).filter((entry) => entry.attachment.content.type === "content");
|
|
238
|
+
if (contentEntries.length === 0) return attachments;
|
|
239
|
+
let queue = new PQueue({ concurrency: 10 });
|
|
240
|
+
let replacements = /* @__PURE__ */ new Map();
|
|
241
|
+
await Promise.all(
|
|
242
|
+
contentEntries.map(
|
|
243
|
+
(entry) => queue.add(async () => {
|
|
244
|
+
try {
|
|
245
|
+
let body = entry.attachment.content.type === "content" ? entry.attachment.content.encoding === "base64" ? Buffer.from(entry.attachment.content.content, "base64") : Buffer.from(entry.attachment.content.content, "utf-8") : Buffer.alloc(0);
|
|
246
|
+
replacements.set(
|
|
247
|
+
entry.index,
|
|
248
|
+
await uploadAttachmentDirect({
|
|
249
|
+
live,
|
|
250
|
+
mimeType: entry.attachment.mimeType,
|
|
251
|
+
body
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
} catch {
|
|
255
|
+
replacements.set(entry.index, null);
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
return attachments.map(
|
|
261
|
+
(attachment, index) => replacements.has(index) ? replacements.get(index) : attachment
|
|
262
|
+
).filter((a) => a != null);
|
|
263
|
+
};
|
|
232
264
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
233
265
|
var getObjectKeyCount = (value) => isRecord(value) ? Object.keys(value).length : void 0;
|
|
234
266
|
var DOWNLOAD_ATTACHMENT_URL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -292,6 +324,19 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
|
|
|
292
324
|
let auth = new State(null);
|
|
293
325
|
let config = new State(null);
|
|
294
326
|
let session = new State(null);
|
|
327
|
+
let hubCapabilities = new State(null);
|
|
328
|
+
let liveInvocation = new State(
|
|
329
|
+
null
|
|
330
|
+
);
|
|
331
|
+
let getLiveInvocation = () => {
|
|
332
|
+
let directUpload = hubCapabilities.get()?.attachments?.directUpload;
|
|
333
|
+
let live = liveInvocation.get();
|
|
334
|
+
if (!directUpload?.enabled || !live) return null;
|
|
335
|
+
return {
|
|
336
|
+
...live,
|
|
337
|
+
maxAttachmentSizeBytes: directUpload.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES
|
|
338
|
+
};
|
|
339
|
+
};
|
|
295
340
|
let logger = new SlateLogger(listeners);
|
|
296
341
|
let providerTrace = {
|
|
297
342
|
providerId: slate.spec.key,
|
|
@@ -418,6 +463,15 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
|
|
|
418
463
|
state: params.state
|
|
419
464
|
});
|
|
420
465
|
});
|
|
466
|
+
manager.onNotification("slates/hub.capabilities.set", async ({ params }) => {
|
|
467
|
+
hubCapabilities.set(params.capabilities);
|
|
468
|
+
});
|
|
469
|
+
manager.onNotification("slates/hub.live_invocation.set", async ({ params }) => {
|
|
470
|
+
liveInvocation.set({
|
|
471
|
+
token: params.token,
|
|
472
|
+
baseUrl: params.baseUrl
|
|
473
|
+
});
|
|
474
|
+
});
|
|
421
475
|
manager.onRequest("slates/config.changed", async ({ params }) => {
|
|
422
476
|
getContextBasic();
|
|
423
477
|
let newConfig = validate(
|
|
@@ -882,19 +936,39 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
|
|
|
882
936
|
},
|
|
883
937
|
() => runWithContext(context, () => action.handleInvocation(context))
|
|
884
938
|
);
|
|
939
|
+
let contextAttachments = await context._finalizeAttachments();
|
|
940
|
+
let merged = mergeAttachments(
|
|
941
|
+
[...res.attachments ?? [], ...contextAttachments],
|
|
942
|
+
res.output
|
|
943
|
+
);
|
|
944
|
+
let redacted = redactUrlAttachmentSecrets(
|
|
945
|
+
merged,
|
|
946
|
+
context._getAuthConfigForRedaction()
|
|
947
|
+
);
|
|
948
|
+
let finalAttachments = await routeAttachmentsThroughDirectUpload(
|
|
949
|
+
redacted,
|
|
950
|
+
getLiveInvocation()
|
|
951
|
+
);
|
|
885
952
|
return withRequestTraces(context, {
|
|
886
953
|
output: res.output,
|
|
887
954
|
message: res.message,
|
|
888
|
-
attachments:
|
|
955
|
+
attachments: finalAttachments
|
|
889
956
|
});
|
|
890
957
|
};
|
|
891
958
|
if (action.isPublic) {
|
|
892
959
|
getContextBasic();
|
|
893
|
-
return invoke(new SlatePublicContext(input, slate.spec, logger));
|
|
960
|
+
return invoke(new SlatePublicContext(input, slate.spec, logger, getLiveInvocation()));
|
|
894
961
|
}
|
|
895
962
|
let ctx = getContextFull();
|
|
896
963
|
return invoke(
|
|
897
|
-
new SlateContext(
|
|
964
|
+
new SlateContext(
|
|
965
|
+
ctx.config,
|
|
966
|
+
input,
|
|
967
|
+
ctx.auth?.output,
|
|
968
|
+
slate.spec,
|
|
969
|
+
logger,
|
|
970
|
+
getLiveInvocation()
|
|
971
|
+
)
|
|
898
972
|
);
|
|
899
973
|
});
|
|
900
974
|
manager.onRequest("slates/action.trigger.map_event", async ({ params }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slates/provider-handler",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.24",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@lowerdeck/error": "^1.1.0",
|
|
35
|
-
"@slates/proto": "1.0.0-rc.
|
|
36
|
-
"@slates/provider": "1.0.0-rc.
|
|
35
|
+
"@slates/proto": "1.0.0-rc.16",
|
|
36
|
+
"@slates/provider": "1.0.0-rc.19",
|
|
37
|
+
"p-queue": "^6.6.2",
|
|
37
38
|
"zod": "^4.2.1"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { SLATES_PROTOCOL_VERSION, SlatesProviderProtoHandlerManager } from '@slates/proto';
|
|
2
|
+
import {
|
|
3
|
+
Slate,
|
|
4
|
+
SlateAuth,
|
|
5
|
+
SlateConfig,
|
|
6
|
+
SlatePublicTool,
|
|
7
|
+
SlateSpecification
|
|
8
|
+
} from '@slates/provider';
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { createProviderHandler } from './index';
|
|
12
|
+
|
|
13
|
+
let createManager = async () => {
|
|
14
|
+
let spec = SlateSpecification.create({
|
|
15
|
+
key: 'attachments-test',
|
|
16
|
+
name: 'Attachments Test',
|
|
17
|
+
config: SlateConfig.create(z.object({})),
|
|
18
|
+
auth: SlateAuth.create().output(z.object({}))
|
|
19
|
+
});
|
|
20
|
+
let tool = SlatePublicTool.create(spec, {
|
|
21
|
+
key: 'attach',
|
|
22
|
+
name: 'Attach'
|
|
23
|
+
})
|
|
24
|
+
.input(z.object({}))
|
|
25
|
+
.output(z.object({}))
|
|
26
|
+
.handleInvocation(async ctx => {
|
|
27
|
+
await ctx.addAttachment({
|
|
28
|
+
type: 'content',
|
|
29
|
+
content: new Response('hello', {
|
|
30
|
+
headers: { 'content-type': 'text/plain' }
|
|
31
|
+
}),
|
|
32
|
+
filename: 'hello.txt'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return { output: {}, message: 'ok' };
|
|
36
|
+
})
|
|
37
|
+
.build();
|
|
38
|
+
let slate = Slate.create({ spec, triggers: [], tools: [tool] });
|
|
39
|
+
let manager = await createProviderHandler(slate, []).run();
|
|
40
|
+
|
|
41
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
42
|
+
jsonrpc: '2.0',
|
|
43
|
+
method: 'slates/hello',
|
|
44
|
+
params: { protocol: SLATES_PROTOCOL_VERSION }
|
|
45
|
+
});
|
|
46
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
47
|
+
jsonrpc: '2.0',
|
|
48
|
+
method: 'slates/participant.set',
|
|
49
|
+
params: {
|
|
50
|
+
participants: [
|
|
51
|
+
{ type: 'consumer', id: 'consumer', name: 'Consumer' },
|
|
52
|
+
{ type: 'hub', id: 'hub', name: 'Hub' }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return manager;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let invoke = async (manager: SlatesProviderProtoHandlerManager) =>
|
|
61
|
+
SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
62
|
+
jsonrpc: '2.0',
|
|
63
|
+
id: 'request',
|
|
64
|
+
method: 'slates/action.tool.invoke',
|
|
65
|
+
params: { actionId: 'attach', input: {} }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let expectInlineAttachment = (response: Awaited<ReturnType<typeof invoke>>) => {
|
|
69
|
+
expect(response).toMatchObject({
|
|
70
|
+
result: {
|
|
71
|
+
attachments: [
|
|
72
|
+
{
|
|
73
|
+
mimeType: 'text/plain',
|
|
74
|
+
content: {
|
|
75
|
+
type: 'content',
|
|
76
|
+
encoding: 'base64',
|
|
77
|
+
content: Buffer.from('hello').toString('base64')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
describe('addAttachment fallback', () => {
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.restoreAllMocks();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns a standard inline attachment when capabilities and a live token are absent', async () => {
|
|
91
|
+
let fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
92
|
+
let response = await invoke(await createManager());
|
|
93
|
+
|
|
94
|
+
expectInlineAttachment(response);
|
|
95
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns a standard inline attachment when direct upload is enabled without a live token', async () => {
|
|
99
|
+
let manager = await createManager();
|
|
100
|
+
let fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
101
|
+
|
|
102
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
103
|
+
jsonrpc: '2.0',
|
|
104
|
+
method: 'slates/hub.capabilities.set',
|
|
105
|
+
params: { capabilities: { attachments: { directUpload: { enabled: true } } } }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expectInlineAttachment(await invoke(manager));
|
|
109
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns a standard inline attachment when a live token exists without the capability', async () => {
|
|
113
|
+
let manager = await createManager();
|
|
114
|
+
let fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
115
|
+
|
|
116
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
method: 'slates/hub.live_invocation.set',
|
|
119
|
+
params: { token: 'token', baseUrl: 'https://hub.example' }
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expectInlineAttachment(await invoke(manager));
|
|
123
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('uses direct upload only when both the capability and live token exist', async () => {
|
|
127
|
+
let manager = await createManager();
|
|
128
|
+
let fetchSpy = vi
|
|
129
|
+
.spyOn(globalThis, 'fetch')
|
|
130
|
+
.mockResolvedValueOnce(
|
|
131
|
+
new Response(
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
attachments: [
|
|
134
|
+
{
|
|
135
|
+
referenceId: 'attachment-reference',
|
|
136
|
+
uploadUrl: 'https://uploads.example/attachment'
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}),
|
|
140
|
+
{ status: 200, headers: { 'content-type': 'application/json' } }
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
144
|
+
|
|
145
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
146
|
+
jsonrpc: '2.0',
|
|
147
|
+
method: 'slates/hub.capabilities.set',
|
|
148
|
+
params: { capabilities: { attachments: { directUpload: { enabled: true } } } }
|
|
149
|
+
});
|
|
150
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
151
|
+
jsonrpc: '2.0',
|
|
152
|
+
method: 'slates/hub.live_invocation.set',
|
|
153
|
+
params: { token: 'token', baseUrl: 'https://hub.example' }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(await invoke(manager)).toMatchObject({
|
|
157
|
+
result: {
|
|
158
|
+
attachments: [
|
|
159
|
+
{
|
|
160
|
+
mimeType: 'text/plain',
|
|
161
|
+
content: {
|
|
162
|
+
type: 'upload_reference',
|
|
163
|
+
referenceId: 'attachment-reference'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
170
|
+
});
|
|
171
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -5,14 +5,18 @@ import {
|
|
|
5
5
|
type SlatesParticipant
|
|
6
6
|
} from '@slates/proto';
|
|
7
7
|
import {
|
|
8
|
+
redactUrlAttachmentSecrets,
|
|
8
9
|
runWithContext,
|
|
9
10
|
type Slate,
|
|
10
11
|
type SlateAttachment,
|
|
11
12
|
SlateContext,
|
|
13
|
+
type SlateLiveInvocationInfo,
|
|
12
14
|
SlateLogger,
|
|
13
15
|
type SlateLogListener,
|
|
14
|
-
SlatePublicContext
|
|
16
|
+
SlatePublicContext,
|
|
17
|
+
uploadAttachmentDirect
|
|
15
18
|
} from '@slates/provider';
|
|
19
|
+
import PQueue from 'p-queue';
|
|
16
20
|
import {
|
|
17
21
|
getAction,
|
|
18
22
|
getActionWithType,
|
|
@@ -26,6 +30,57 @@ import { State } from './state';
|
|
|
26
30
|
import { toJsonSchema, validate } from './validation';
|
|
27
31
|
import { serializeWebhookHttpResponse } from './webhook';
|
|
28
32
|
|
|
33
|
+
let DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 100_000_000;
|
|
34
|
+
|
|
35
|
+
let routeAttachmentsThroughDirectUpload = async (
|
|
36
|
+
attachments: SlateAttachment[] | undefined,
|
|
37
|
+
live: SlateLiveInvocationInfo | null
|
|
38
|
+
): Promise<SlateAttachment[] | undefined> => {
|
|
39
|
+
if (!attachments || attachments.length === 0 || !live) return attachments;
|
|
40
|
+
|
|
41
|
+
let contentEntries = attachments
|
|
42
|
+
.map((attachment, index) => ({ attachment, index }))
|
|
43
|
+
.filter(entry => entry.attachment.content.type === 'content');
|
|
44
|
+
if (contentEntries.length === 0) return attachments;
|
|
45
|
+
|
|
46
|
+
let queue = new PQueue({ concurrency: 10 });
|
|
47
|
+
let replacements = new Map<number, SlateAttachment | null>();
|
|
48
|
+
|
|
49
|
+
await Promise.all(
|
|
50
|
+
contentEntries.map(entry =>
|
|
51
|
+
queue.add(async () => {
|
|
52
|
+
try {
|
|
53
|
+
let body =
|
|
54
|
+
entry.attachment.content.type === 'content'
|
|
55
|
+
? entry.attachment.content.encoding === 'base64'
|
|
56
|
+
? Buffer.from(entry.attachment.content.content, 'base64')
|
|
57
|
+
: Buffer.from(entry.attachment.content.content, 'utf-8')
|
|
58
|
+
: Buffer.alloc(0);
|
|
59
|
+
|
|
60
|
+
replacements.set(
|
|
61
|
+
entry.index,
|
|
62
|
+
await uploadAttachmentDirect({
|
|
63
|
+
live,
|
|
64
|
+
mimeType: entry.attachment.mimeType,
|
|
65
|
+
body
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
// Upload failed or was interrupted -- we simply don't have this attachment, not a
|
|
70
|
+
// failed tool call.
|
|
71
|
+
replacements.set(entry.index, null);
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return attachments
|
|
78
|
+
.map((attachment, index) =>
|
|
79
|
+
replacements.has(index) ? replacements.get(index) : attachment
|
|
80
|
+
)
|
|
81
|
+
.filter((a): a is SlateAttachment => a != null);
|
|
82
|
+
};
|
|
83
|
+
|
|
29
84
|
let isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
30
85
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
31
86
|
|
|
@@ -128,6 +183,26 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
|
|
|
128
183
|
let config = new State<{ value: ConfigType } | null>(null);
|
|
129
184
|
let session = new State<{ id: string; state: any } | null>(null);
|
|
130
185
|
|
|
186
|
+
let hubCapabilities = new State<{
|
|
187
|
+
attachments?: { directUpload?: { enabled: boolean; maxAttachmentSizeBytes?: number } };
|
|
188
|
+
} | null>(null);
|
|
189
|
+
let liveInvocation = new State<Pick<SlateLiveInvocationInfo, 'token' | 'baseUrl'> | null>(
|
|
190
|
+
null
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
let getLiveInvocation = (): SlateLiveInvocationInfo | null => {
|
|
194
|
+
let directUpload = hubCapabilities.get()?.attachments?.directUpload;
|
|
195
|
+
let live = liveInvocation.get();
|
|
196
|
+
|
|
197
|
+
if (!directUpload?.enabled || !live) return null;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...live,
|
|
201
|
+
maxAttachmentSizeBytes:
|
|
202
|
+
directUpload.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
131
206
|
let logger = new SlateLogger(listeners);
|
|
132
207
|
let providerTrace = {
|
|
133
208
|
providerId: slate.spec.key,
|
|
@@ -301,6 +376,17 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
|
|
|
301
376
|
});
|
|
302
377
|
});
|
|
303
378
|
|
|
379
|
+
manager.onNotification('slates/hub.capabilities.set', async ({ params }) => {
|
|
380
|
+
hubCapabilities.set(params.capabilities);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
manager.onNotification('slates/hub.live_invocation.set', async ({ params }) => {
|
|
384
|
+
liveInvocation.set({
|
|
385
|
+
token: params.token,
|
|
386
|
+
baseUrl: params.baseUrl
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
304
390
|
manager.onRequest('slates/config.changed', async ({ params }) => {
|
|
305
391
|
getContextBasic();
|
|
306
392
|
|
|
@@ -828,21 +914,43 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
|
|
|
828
914
|
() => runWithContext(context, () => action.handleInvocation(context as any))
|
|
829
915
|
);
|
|
830
916
|
|
|
917
|
+
let contextAttachments = await context._finalizeAttachments();
|
|
918
|
+
let merged = mergeAttachments(
|
|
919
|
+
[...(res.attachments ?? []), ...contextAttachments],
|
|
920
|
+
res.output
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
let redacted = redactUrlAttachmentSecrets(
|
|
924
|
+
merged,
|
|
925
|
+
context._getAuthConfigForRedaction()
|
|
926
|
+
);
|
|
927
|
+
let finalAttachments = await routeAttachmentsThroughDirectUpload(
|
|
928
|
+
redacted,
|
|
929
|
+
getLiveInvocation()
|
|
930
|
+
);
|
|
931
|
+
|
|
831
932
|
return withRequestTraces(context, {
|
|
832
933
|
output: res.output,
|
|
833
934
|
message: res.message,
|
|
834
|
-
attachments:
|
|
935
|
+
attachments: finalAttachments
|
|
835
936
|
});
|
|
836
937
|
};
|
|
837
938
|
|
|
838
939
|
if (action.isPublic) {
|
|
839
940
|
getContextBasic();
|
|
840
|
-
return invoke(new SlatePublicContext(input, slate.spec, logger));
|
|
941
|
+
return invoke(new SlatePublicContext(input, slate.spec, logger, getLiveInvocation()));
|
|
841
942
|
}
|
|
842
943
|
|
|
843
944
|
let ctx = getContextFull();
|
|
844
945
|
return invoke(
|
|
845
|
-
new SlateContext(
|
|
946
|
+
new SlateContext(
|
|
947
|
+
ctx.config,
|
|
948
|
+
input,
|
|
949
|
+
ctx.auth?.output!,
|
|
950
|
+
slate.spec,
|
|
951
|
+
logger,
|
|
952
|
+
getLiveInvocation()
|
|
953
|
+
)
|
|
846
954
|
);
|
|
847
955
|
});
|
|
848
956
|
|
package/src/spec.ts
CHANGED