@slates/provider-handler 1.0.0-rc.22 → 1.0.0-rc.26
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 +92 -5
- package/dist/index.module.js +95 -6
- package/package.json +4 -3
- package/src/attachments.test.ts +171 -0
- package/src/index.ts +112 -4
- package/src/pQueue.test.ts +18 -0
- package/src/pQueue.ts +18 -0
- package/src/spec.ts +1 -2
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,22 @@ var import_error4 = require("@lowerdeck/error");
|
|
|
37
37
|
var import_proto = require("@slates/proto");
|
|
38
38
|
var import_provider2 = require("@slates/provider");
|
|
39
39
|
|
|
40
|
+
// src/pQueue.ts
|
|
41
|
+
var import_p_queue = __toESM(require("p-queue"), 1);
|
|
42
|
+
var resolveDefaultExport = (value) => {
|
|
43
|
+
let current = value;
|
|
44
|
+
for (let i = 0; i < 4; i++) {
|
|
45
|
+
if (typeof current === "function") return current;
|
|
46
|
+
if (current && typeof current === "object" && "default" in current) {
|
|
47
|
+
current = current.default;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
throw new TypeError("Module export is not a constructor");
|
|
53
|
+
};
|
|
54
|
+
var PQueue = resolveDefaultExport(import_p_queue.default);
|
|
55
|
+
|
|
40
56
|
// src/spec.ts
|
|
41
57
|
var import_error2 = require("@lowerdeck/error");
|
|
42
58
|
var import_provider = require("@slates/provider");
|
|
@@ -94,8 +110,7 @@ var mapAuthMethod = (slate, m) => ({
|
|
|
94
110
|
scopes: "scopes" in m ? m.scopes.map((s) => ({
|
|
95
111
|
id: s.scope,
|
|
96
112
|
title: s.title,
|
|
97
|
-
description: s.description
|
|
98
|
-
defaultChecked: s.defaultChecked
|
|
113
|
+
description: s.description
|
|
99
114
|
})) : void 0,
|
|
100
115
|
inputSchema: toJsonSchema(m.inputSchema ?? import_zod.default.object({})),
|
|
101
116
|
outputSchema: toJsonSchema(slate.spec.auth.outputSchema),
|
|
@@ -253,6 +268,36 @@ var serializeWebhookHttpResponse = async (response) => {
|
|
|
253
268
|
};
|
|
254
269
|
|
|
255
270
|
// src/index.ts
|
|
271
|
+
var DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 1e8;
|
|
272
|
+
var routeAttachmentsThroughDirectUpload = async (attachments, live) => {
|
|
273
|
+
if (!attachments || attachments.length === 0 || !live) return attachments;
|
|
274
|
+
let contentEntries = attachments.map((attachment, index) => ({ attachment, index })).filter((entry) => entry.attachment.content.type === "content");
|
|
275
|
+
if (contentEntries.length === 0) return attachments;
|
|
276
|
+
let queue = new PQueue({ concurrency: 10 });
|
|
277
|
+
let replacements = /* @__PURE__ */ new Map();
|
|
278
|
+
await Promise.all(
|
|
279
|
+
contentEntries.map(
|
|
280
|
+
(entry) => queue.add(async () => {
|
|
281
|
+
try {
|
|
282
|
+
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);
|
|
283
|
+
replacements.set(
|
|
284
|
+
entry.index,
|
|
285
|
+
await (0, import_provider2.uploadAttachmentDirect)({
|
|
286
|
+
live,
|
|
287
|
+
mimeType: entry.attachment.mimeType,
|
|
288
|
+
body
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
} catch {
|
|
292
|
+
replacements.set(entry.index, null);
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
return attachments.map(
|
|
298
|
+
(attachment, index) => replacements.has(index) ? replacements.get(index) : attachment
|
|
299
|
+
).filter((a) => a != null);
|
|
300
|
+
};
|
|
256
301
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
257
302
|
var getObjectKeyCount = (value) => isRecord(value) ? Object.keys(value).length : void 0;
|
|
258
303
|
var DOWNLOAD_ATTACHMENT_URL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -316,6 +361,19 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
|
|
|
316
361
|
let auth = new State(null);
|
|
317
362
|
let config = new State(null);
|
|
318
363
|
let session = new State(null);
|
|
364
|
+
let hubCapabilities = new State(null);
|
|
365
|
+
let liveInvocation = new State(
|
|
366
|
+
null
|
|
367
|
+
);
|
|
368
|
+
let getLiveInvocation = () => {
|
|
369
|
+
let directUpload = hubCapabilities.get()?.attachments?.directUpload;
|
|
370
|
+
let live = liveInvocation.get();
|
|
371
|
+
if (!directUpload?.enabled || !live) return null;
|
|
372
|
+
return {
|
|
373
|
+
...live,
|
|
374
|
+
maxAttachmentSizeBytes: directUpload.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES
|
|
375
|
+
};
|
|
376
|
+
};
|
|
319
377
|
let logger = new import_provider2.SlateLogger(listeners);
|
|
320
378
|
let providerTrace = {
|
|
321
379
|
providerId: slate.spec.key,
|
|
@@ -442,6 +500,15 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
|
|
|
442
500
|
state: params.state
|
|
443
501
|
});
|
|
444
502
|
});
|
|
503
|
+
manager.onNotification("slates/hub.capabilities.set", async ({ params }) => {
|
|
504
|
+
hubCapabilities.set(params.capabilities);
|
|
505
|
+
});
|
|
506
|
+
manager.onNotification("slates/hub.live_invocation.set", async ({ params }) => {
|
|
507
|
+
liveInvocation.set({
|
|
508
|
+
token: params.token,
|
|
509
|
+
baseUrl: params.baseUrl
|
|
510
|
+
});
|
|
511
|
+
});
|
|
445
512
|
manager.onRequest("slates/config.changed", async ({ params }) => {
|
|
446
513
|
getContextBasic();
|
|
447
514
|
let newConfig = validate(
|
|
@@ -906,19 +973,39 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
|
|
|
906
973
|
},
|
|
907
974
|
() => (0, import_provider2.runWithContext)(context, () => action.handleInvocation(context))
|
|
908
975
|
);
|
|
976
|
+
let contextAttachments = await context._finalizeAttachments();
|
|
977
|
+
let merged = mergeAttachments(
|
|
978
|
+
[...res.attachments ?? [], ...contextAttachments],
|
|
979
|
+
res.output
|
|
980
|
+
);
|
|
981
|
+
let redacted = (0, import_provider2.redactUrlAttachmentSecrets)(
|
|
982
|
+
merged,
|
|
983
|
+
context._getAuthConfigForRedaction()
|
|
984
|
+
);
|
|
985
|
+
let finalAttachments = await routeAttachmentsThroughDirectUpload(
|
|
986
|
+
redacted,
|
|
987
|
+
getLiveInvocation()
|
|
988
|
+
);
|
|
909
989
|
return withRequestTraces(context, {
|
|
910
990
|
output: res.output,
|
|
911
991
|
message: res.message,
|
|
912
|
-
attachments:
|
|
992
|
+
attachments: finalAttachments
|
|
913
993
|
});
|
|
914
994
|
};
|
|
915
995
|
if (action.isPublic) {
|
|
916
996
|
getContextBasic();
|
|
917
|
-
return invoke(new import_provider2.SlatePublicContext(input, slate.spec, logger));
|
|
997
|
+
return invoke(new import_provider2.SlatePublicContext(input, slate.spec, logger, getLiveInvocation()));
|
|
918
998
|
}
|
|
919
999
|
let ctx = getContextFull();
|
|
920
1000
|
return invoke(
|
|
921
|
-
new import_provider2.SlateContext(
|
|
1001
|
+
new import_provider2.SlateContext(
|
|
1002
|
+
ctx.config,
|
|
1003
|
+
input,
|
|
1004
|
+
ctx.auth?.output,
|
|
1005
|
+
slate.spec,
|
|
1006
|
+
logger,
|
|
1007
|
+
getLiveInvocation()
|
|
1008
|
+
)
|
|
922
1009
|
);
|
|
923
1010
|
});
|
|
924
1011
|
manager.onRequest("slates/action.trigger.map_event", async ({ params }) => {
|
package/dist/index.module.js
CHANGED
|
@@ -5,12 +5,30 @@ 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";
|
|
13
15
|
|
|
16
|
+
// src/pQueue.ts
|
|
17
|
+
import PQueueImport from "p-queue";
|
|
18
|
+
var resolveDefaultExport = (value) => {
|
|
19
|
+
let current = value;
|
|
20
|
+
for (let i = 0; i < 4; i++) {
|
|
21
|
+
if (typeof current === "function") return current;
|
|
22
|
+
if (current && typeof current === "object" && "default" in current) {
|
|
23
|
+
current = current.default;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
throw new TypeError("Module export is not a constructor");
|
|
29
|
+
};
|
|
30
|
+
var PQueue = resolveDefaultExport(PQueueImport);
|
|
31
|
+
|
|
14
32
|
// src/spec.ts
|
|
15
33
|
import { badRequestError, notFoundError, ServiceError as ServiceError2 } from "@lowerdeck/error";
|
|
16
34
|
import {
|
|
@@ -70,8 +88,7 @@ var mapAuthMethod = (slate, m) => ({
|
|
|
70
88
|
scopes: "scopes" in m ? m.scopes.map((s) => ({
|
|
71
89
|
id: s.scope,
|
|
72
90
|
title: s.title,
|
|
73
|
-
description: s.description
|
|
74
|
-
defaultChecked: s.defaultChecked
|
|
91
|
+
description: s.description
|
|
75
92
|
})) : void 0,
|
|
76
93
|
inputSchema: toJsonSchema(m.inputSchema ?? z.object({})),
|
|
77
94
|
outputSchema: toJsonSchema(slate.spec.auth.outputSchema),
|
|
@@ -229,6 +246,36 @@ var serializeWebhookHttpResponse = async (response) => {
|
|
|
229
246
|
};
|
|
230
247
|
|
|
231
248
|
// src/index.ts
|
|
249
|
+
var DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 1e8;
|
|
250
|
+
var routeAttachmentsThroughDirectUpload = async (attachments, live) => {
|
|
251
|
+
if (!attachments || attachments.length === 0 || !live) return attachments;
|
|
252
|
+
let contentEntries = attachments.map((attachment, index) => ({ attachment, index })).filter((entry) => entry.attachment.content.type === "content");
|
|
253
|
+
if (contentEntries.length === 0) return attachments;
|
|
254
|
+
let queue = new PQueue({ concurrency: 10 });
|
|
255
|
+
let replacements = /* @__PURE__ */ new Map();
|
|
256
|
+
await Promise.all(
|
|
257
|
+
contentEntries.map(
|
|
258
|
+
(entry) => queue.add(async () => {
|
|
259
|
+
try {
|
|
260
|
+
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);
|
|
261
|
+
replacements.set(
|
|
262
|
+
entry.index,
|
|
263
|
+
await uploadAttachmentDirect({
|
|
264
|
+
live,
|
|
265
|
+
mimeType: entry.attachment.mimeType,
|
|
266
|
+
body
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
} catch {
|
|
270
|
+
replacements.set(entry.index, null);
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
return attachments.map(
|
|
276
|
+
(attachment, index) => replacements.has(index) ? replacements.get(index) : attachment
|
|
277
|
+
).filter((a) => a != null);
|
|
278
|
+
};
|
|
232
279
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
233
280
|
var getObjectKeyCount = (value) => isRecord(value) ? Object.keys(value).length : void 0;
|
|
234
281
|
var DOWNLOAD_ATTACHMENT_URL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -292,6 +339,19 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
|
|
|
292
339
|
let auth = new State(null);
|
|
293
340
|
let config = new State(null);
|
|
294
341
|
let session = new State(null);
|
|
342
|
+
let hubCapabilities = new State(null);
|
|
343
|
+
let liveInvocation = new State(
|
|
344
|
+
null
|
|
345
|
+
);
|
|
346
|
+
let getLiveInvocation = () => {
|
|
347
|
+
let directUpload = hubCapabilities.get()?.attachments?.directUpload;
|
|
348
|
+
let live = liveInvocation.get();
|
|
349
|
+
if (!directUpload?.enabled || !live) return null;
|
|
350
|
+
return {
|
|
351
|
+
...live,
|
|
352
|
+
maxAttachmentSizeBytes: directUpload.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES
|
|
353
|
+
};
|
|
354
|
+
};
|
|
295
355
|
let logger = new SlateLogger(listeners);
|
|
296
356
|
let providerTrace = {
|
|
297
357
|
providerId: slate.spec.key,
|
|
@@ -418,6 +478,15 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
|
|
|
418
478
|
state: params.state
|
|
419
479
|
});
|
|
420
480
|
});
|
|
481
|
+
manager.onNotification("slates/hub.capabilities.set", async ({ params }) => {
|
|
482
|
+
hubCapabilities.set(params.capabilities);
|
|
483
|
+
});
|
|
484
|
+
manager.onNotification("slates/hub.live_invocation.set", async ({ params }) => {
|
|
485
|
+
liveInvocation.set({
|
|
486
|
+
token: params.token,
|
|
487
|
+
baseUrl: params.baseUrl
|
|
488
|
+
});
|
|
489
|
+
});
|
|
421
490
|
manager.onRequest("slates/config.changed", async ({ params }) => {
|
|
422
491
|
getContextBasic();
|
|
423
492
|
let newConfig = validate(
|
|
@@ -882,19 +951,39 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
|
|
|
882
951
|
},
|
|
883
952
|
() => runWithContext(context, () => action.handleInvocation(context))
|
|
884
953
|
);
|
|
954
|
+
let contextAttachments = await context._finalizeAttachments();
|
|
955
|
+
let merged = mergeAttachments(
|
|
956
|
+
[...res.attachments ?? [], ...contextAttachments],
|
|
957
|
+
res.output
|
|
958
|
+
);
|
|
959
|
+
let redacted = redactUrlAttachmentSecrets(
|
|
960
|
+
merged,
|
|
961
|
+
context._getAuthConfigForRedaction()
|
|
962
|
+
);
|
|
963
|
+
let finalAttachments = await routeAttachmentsThroughDirectUpload(
|
|
964
|
+
redacted,
|
|
965
|
+
getLiveInvocation()
|
|
966
|
+
);
|
|
885
967
|
return withRequestTraces(context, {
|
|
886
968
|
output: res.output,
|
|
887
969
|
message: res.message,
|
|
888
|
-
attachments:
|
|
970
|
+
attachments: finalAttachments
|
|
889
971
|
});
|
|
890
972
|
};
|
|
891
973
|
if (action.isPublic) {
|
|
892
974
|
getContextBasic();
|
|
893
|
-
return invoke(new SlatePublicContext(input, slate.spec, logger));
|
|
975
|
+
return invoke(new SlatePublicContext(input, slate.spec, logger, getLiveInvocation()));
|
|
894
976
|
}
|
|
895
977
|
let ctx = getContextFull();
|
|
896
978
|
return invoke(
|
|
897
|
-
new SlateContext(
|
|
979
|
+
new SlateContext(
|
|
980
|
+
ctx.config,
|
|
981
|
+
input,
|
|
982
|
+
ctx.auth?.output,
|
|
983
|
+
slate.spec,
|
|
984
|
+
logger,
|
|
985
|
+
getLiveInvocation()
|
|
986
|
+
)
|
|
898
987
|
);
|
|
899
988
|
});
|
|
900
989
|
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.26",
|
|
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.20",
|
|
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 './pQueue';
|
|
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
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveDefaultExport } from './pQueue';
|
|
3
|
+
|
|
4
|
+
class Example {
|
|
5
|
+
value: number;
|
|
6
|
+
constructor(value: number) {
|
|
7
|
+
this.value = value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('resolveDefaultExport', () => {
|
|
12
|
+
it('unwraps nested CJS default exports from bundlers', () => {
|
|
13
|
+
expect(resolveDefaultExport<typeof Example>({ default: { default: Example } })).toBe(
|
|
14
|
+
Example
|
|
15
|
+
);
|
|
16
|
+
expect(new (resolveDefaultExport<typeof Example>({ default: Example }))(3).value).toBe(3);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/pQueue.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import PQueueImport from 'p-queue';
|
|
2
|
+
|
|
3
|
+
export let resolveDefaultExport = <T>(value: unknown): T => {
|
|
4
|
+
let current = value;
|
|
5
|
+
|
|
6
|
+
for (let i = 0; i < 4; i++) {
|
|
7
|
+
if (typeof current === 'function') return current as T;
|
|
8
|
+
if (current && typeof current === 'object' && 'default' in current) {
|
|
9
|
+
current = (current as { default: unknown }).default;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
throw new TypeError('Module export is not a constructor');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export let PQueue = resolveDefaultExport<typeof PQueueImport>(PQueueImport);
|
package/src/spec.ts
CHANGED