@slates/provider 1.0.0-rc.17 → 1.0.0-rc.19
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 +569 -55
- package/dist/index.d.cts +218 -48
- package/dist/index.d.ts +218 -48
- package/dist/index.module.js +554 -55
- package/package.json +3 -2
- package/src/action/action.ts +19 -1
- package/src/action/attachment.ts +55 -9
- package/src/action/builder.test.ts +136 -0
- package/src/action/builder.ts +68 -11
- package/src/action/directUpload.ts +59 -0
- package/src/action/getFileUrlTool.ts +41 -0
- package/src/action/index.ts +3 -1
- package/src/action/publicTool.ts +36 -0
- package/src/action/tool.ts +15 -3
- package/src/adapter/adapter.ts +38 -0
- package/src/adapter/index.test.ts +322 -0
- package/src/adapter/index.ts +2 -0
- package/src/adapter/spec.ts +154 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/redact.test.ts +103 -0
- package/src/auth/redact.ts +111 -0
- package/src/auth/types.ts +0 -1
- package/src/axios/trace.ts +72 -38
- package/src/context/context.ts +222 -17
- package/src/context/hook.ts +5 -10
- package/src/index.ts +2 -0
- package/src/specification/slate.ts +38 -2
package/src/axios/trace.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
|
2
|
-
import
|
|
2
|
+
import { AuthConfigSecretRedactor } from '../auth/redact';
|
|
3
|
+
import type { SlatePublicContext } from '../context';
|
|
3
4
|
|
|
4
5
|
export interface SlateHttpTraceTextBody {
|
|
5
6
|
contentType?: string;
|
|
@@ -29,7 +30,7 @@ export interface SlateHttpTrace {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
interface SlateHttpTraceDraft {
|
|
32
|
-
context:
|
|
33
|
+
context: SlatePublicContext<any>;
|
|
33
34
|
startedAt: string;
|
|
34
35
|
startedAtMs: number;
|
|
35
36
|
request: SlateHttpTrace['request'];
|
|
@@ -377,10 +378,12 @@ let truncateText = (text: string) => {
|
|
|
377
378
|
};
|
|
378
379
|
};
|
|
379
380
|
|
|
380
|
-
let sanitizeFreeText = (text: string) => {
|
|
381
|
+
let sanitizeFreeText = (text: string, redactor?: AuthConfigSecretRedactor) => {
|
|
381
382
|
if (!text) return text;
|
|
382
383
|
|
|
383
|
-
|
|
384
|
+
// Literal auth-config secret values are replaced first -- this catches secrets sitting
|
|
385
|
+
// under a key name the heuristics below don't recognize as secret-like.
|
|
386
|
+
let result = redactor ? redactor.redact(text) : text;
|
|
384
387
|
|
|
385
388
|
result = result.replace(
|
|
386
389
|
HTTP_AUTH_SCHEME_PATTERN,
|
|
@@ -406,9 +409,14 @@ let sanitizeFreeText = (text: string) => {
|
|
|
406
409
|
return result;
|
|
407
410
|
};
|
|
408
411
|
|
|
409
|
-
let sanitizeScalar = (value: string) =>
|
|
412
|
+
let sanitizeScalar = (value: string, redactor?: AuthConfigSecretRedactor) =>
|
|
413
|
+
truncateText(sanitizeFreeText(value, redactor));
|
|
410
414
|
|
|
411
|
-
let redactStructuredValue = (
|
|
415
|
+
let redactStructuredValue = (
|
|
416
|
+
value: unknown,
|
|
417
|
+
depth = 0,
|
|
418
|
+
redactor?: AuthConfigSecretRedactor
|
|
419
|
+
): unknown => {
|
|
412
420
|
if (value == null) return value;
|
|
413
421
|
if (depth >= STRUCTURED_DEPTH_LIMIT) return '[truncated]';
|
|
414
422
|
|
|
@@ -418,13 +426,13 @@ let redactStructuredValue = (value: unknown, depth = 0): unknown => {
|
|
|
418
426
|
typeof value === 'boolean' ||
|
|
419
427
|
typeof value === 'bigint'
|
|
420
428
|
) {
|
|
421
|
-
return typeof value === 'string' ? sanitizeFreeText(value) : value;
|
|
429
|
+
return typeof value === 'string' ? sanitizeFreeText(value, redactor) : value;
|
|
422
430
|
}
|
|
423
431
|
|
|
424
432
|
if (Array.isArray(value)) {
|
|
425
433
|
return value
|
|
426
434
|
.slice(0, STRUCTURED_ENTRY_LIMIT)
|
|
427
|
-
.map(entry => redactStructuredValue(entry, depth + 1));
|
|
435
|
+
.map(entry => redactStructuredValue(entry, depth + 1, redactor));
|
|
428
436
|
}
|
|
429
437
|
|
|
430
438
|
if (isRecord(value)) {
|
|
@@ -433,7 +441,9 @@ let redactStructuredValue = (value: unknown, depth = 0): unknown => {
|
|
|
433
441
|
.slice(0, STRUCTURED_ENTRY_LIMIT)
|
|
434
442
|
.map(([key, entry]) => [
|
|
435
443
|
key,
|
|
436
|
-
isSecretKeyName(key)
|
|
444
|
+
isSecretKeyName(key)
|
|
445
|
+
? REDACTED_VALUE
|
|
446
|
+
: redactStructuredValue(entry, depth + 1, redactor)
|
|
437
447
|
])
|
|
438
448
|
);
|
|
439
449
|
}
|
|
@@ -457,7 +467,8 @@ let maybeParseJson = (text: string, contentType?: string) => {
|
|
|
457
467
|
|
|
458
468
|
let sanitizeTextBody = (
|
|
459
469
|
text: string,
|
|
460
|
-
contentType?: string
|
|
470
|
+
contentType?: string,
|
|
471
|
+
redactor?: AuthConfigSecretRedactor
|
|
461
472
|
): SlateHttpTraceTextBody | undefined => {
|
|
462
473
|
if (!text) return undefined;
|
|
463
474
|
|
|
@@ -465,8 +476,8 @@ let sanitizeTextBody = (
|
|
|
465
476
|
let parsedJson = maybeParseJson(text, normalizedContentType);
|
|
466
477
|
let normalizedText =
|
|
467
478
|
parsedJson !== null
|
|
468
|
-
? JSON.stringify(redactStructuredValue(parsedJson))
|
|
469
|
-
: sanitizeFreeText(text);
|
|
479
|
+
? JSON.stringify(redactStructuredValue(parsedJson, 0, redactor))
|
|
480
|
+
: sanitizeFreeText(text, redactor);
|
|
470
481
|
let { text: truncatedText, truncated } = truncateText(normalizedText);
|
|
471
482
|
|
|
472
483
|
return {
|
|
@@ -487,7 +498,8 @@ let isBinaryLike = (value: unknown) => {
|
|
|
487
498
|
|
|
488
499
|
let serializeTextBody = (
|
|
489
500
|
value: unknown,
|
|
490
|
-
contentType?: string
|
|
501
|
+
contentType?: string,
|
|
502
|
+
redactor?: AuthConfigSecretRedactor
|
|
491
503
|
): SlateHttpTraceTextBody | undefined => {
|
|
492
504
|
let normalizedContentType = normalizeContentType(contentType);
|
|
493
505
|
|
|
@@ -496,19 +508,23 @@ let serializeTextBody = (
|
|
|
496
508
|
}
|
|
497
509
|
|
|
498
510
|
if (typeof value === 'string') {
|
|
499
|
-
return sanitizeTextBody(value, normalizedContentType);
|
|
511
|
+
return sanitizeTextBody(value, normalizedContentType, redactor);
|
|
500
512
|
}
|
|
501
513
|
|
|
502
514
|
if (value instanceof URLSearchParams) {
|
|
503
|
-
return sanitizeTextBody(value.toString(), normalizedContentType);
|
|
515
|
+
return sanitizeTextBody(value.toString(), normalizedContentType, redactor);
|
|
504
516
|
}
|
|
505
517
|
|
|
506
518
|
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
507
|
-
return sanitizeTextBody(String(value), normalizedContentType);
|
|
519
|
+
return sanitizeTextBody(String(value), normalizedContentType, redactor);
|
|
508
520
|
}
|
|
509
521
|
|
|
510
522
|
if (Array.isArray(value) || isRecord(value)) {
|
|
511
|
-
return sanitizeTextBody(
|
|
523
|
+
return sanitizeTextBody(
|
|
524
|
+
JSON.stringify(redactStructuredValue(value, 0, redactor)),
|
|
525
|
+
'application/json',
|
|
526
|
+
redactor
|
|
527
|
+
);
|
|
512
528
|
}
|
|
513
529
|
|
|
514
530
|
return undefined;
|
|
@@ -533,18 +549,21 @@ let decodeRedactedMarker = (value: string) =>
|
|
|
533
549
|
? value
|
|
534
550
|
: value.split(REDACTED_ENCODED).join(REDACTED_VALUE);
|
|
535
551
|
|
|
536
|
-
let redactHashFragment = (hash: string) => {
|
|
552
|
+
let redactHashFragment = (hash: string, redactor?: AuthConfigSecretRedactor) => {
|
|
537
553
|
if (!hash?.includes('=')) return hash;
|
|
538
554
|
let withoutHash = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
539
555
|
let redacted = withoutHash.replace(
|
|
540
556
|
/(?<![A-Za-z0-9])([A-Za-z0-9][A-Za-z0-9_\-.]*)=([^&]*)/g,
|
|
541
|
-
(match, key: string,
|
|
542
|
-
isUrlFormSecretKeyName(key)
|
|
557
|
+
(match, key: string, rawValue: string) => {
|
|
558
|
+
if (isUrlFormSecretKeyName(key)) return `${key}=${REDACTED_VALUE}`;
|
|
559
|
+
let literalRedacted = redactor ? redactor.redact(rawValue) : rawValue;
|
|
560
|
+
return literalRedacted !== rawValue ? `${key}=${literalRedacted}` : match;
|
|
561
|
+
}
|
|
543
562
|
);
|
|
544
563
|
return redacted ? `#${redacted}` : '';
|
|
545
564
|
};
|
|
546
565
|
|
|
547
|
-
let sanitizeUrl = (value: string) => {
|
|
566
|
+
let sanitizeUrl = (value: string, redactor?: AuthConfigSecretRedactor) => {
|
|
548
567
|
if (!value) return value;
|
|
549
568
|
|
|
550
569
|
try {
|
|
@@ -558,14 +577,21 @@ let sanitizeUrl = (value: string) => {
|
|
|
558
577
|
for (let key of Array.from(url.searchParams.keys())) {
|
|
559
578
|
if (isUrlFormSecretKeyName(key)) {
|
|
560
579
|
url.searchParams.set(key, REDACTED_VALUE);
|
|
580
|
+
continue;
|
|
561
581
|
}
|
|
582
|
+
|
|
583
|
+
// Not a secret-looking key -- still catch a literal auth-config secret value.
|
|
584
|
+
let rawValue = url.searchParams.get(key) ?? '';
|
|
585
|
+
let literalRedacted = redactor ? redactor.redact(rawValue) : rawValue;
|
|
586
|
+
if (literalRedacted !== rawValue) url.searchParams.set(key, literalRedacted);
|
|
562
587
|
}
|
|
563
588
|
|
|
564
|
-
url.hash = redactHashFragment(url.hash);
|
|
589
|
+
url.hash = redactHashFragment(url.hash, redactor);
|
|
565
590
|
|
|
566
591
|
return decodeRedactedMarker(url.toString());
|
|
567
592
|
} catch {
|
|
568
|
-
|
|
593
|
+
let text = redactor ? redactor.redact(value) : value;
|
|
594
|
+
return text.replace(
|
|
569
595
|
/([?&]([^=&#]+)=)([^&#]+)/g,
|
|
570
596
|
(_match, prefix: string, key: string, rawValue: string) =>
|
|
571
597
|
isUrlFormSecretKeyName(key) ? `${prefix}${REDACTED_VALUE}` : `${prefix}${rawValue}`
|
|
@@ -618,9 +644,12 @@ let headerEntries = (headers: unknown): [string, string][] => {
|
|
|
618
644
|
return [];
|
|
619
645
|
};
|
|
620
646
|
|
|
621
|
-
let sanitizeHeaders = (headers: unknown) => {
|
|
647
|
+
let sanitizeHeaders = (headers: unknown, redactor?: AuthConfigSecretRedactor) => {
|
|
622
648
|
let entries = headerEntries(headers)
|
|
623
|
-
.map(
|
|
649
|
+
.map(
|
|
650
|
+
([key, value]) =>
|
|
651
|
+
[key.toLowerCase(), sanitizeScalar(value, redactor).text] as [string, string]
|
|
652
|
+
)
|
|
624
653
|
.filter(([key]) => isSafeHeaderName(key));
|
|
625
654
|
|
|
626
655
|
if (entries.length === 0) return undefined;
|
|
@@ -664,10 +693,11 @@ let clearTraceDraft = (
|
|
|
664
693
|
|
|
665
694
|
export let attachHttpTraceDraft = (
|
|
666
695
|
config: InternalAxiosRequestConfig,
|
|
667
|
-
context:
|
|
696
|
+
context: SlatePublicContext<any>
|
|
668
697
|
) => {
|
|
669
698
|
let traceAwareConfig = config as TraceAwareAxiosRequestConfig;
|
|
670
699
|
let contentType = getContentType(config.headers);
|
|
700
|
+
let redactor = new AuthConfigSecretRedactor(context._getAuthConfigForRedaction());
|
|
671
701
|
|
|
672
702
|
traceAwareConfig.__slatesHttpTraceDraft = {
|
|
673
703
|
context,
|
|
@@ -675,10 +705,12 @@ export let attachHttpTraceDraft = (
|
|
|
675
705
|
startedAtMs: Date.now(),
|
|
676
706
|
request: {
|
|
677
707
|
method: (config.method ?? 'GET').toUpperCase(),
|
|
678
|
-
url: sanitizeUrl(buildUrl(config.baseURL, config.url)),
|
|
679
|
-
...(sanitizeHeaders(config.headers
|
|
680
|
-
|
|
681
|
-
|
|
708
|
+
url: sanitizeUrl(buildUrl(config.baseURL, config.url), redactor),
|
|
709
|
+
...(sanitizeHeaders(config.headers, redactor)
|
|
710
|
+
? { headers: sanitizeHeaders(config.headers, redactor) }
|
|
711
|
+
: {}),
|
|
712
|
+
...(serializeTextBody(config.data, contentType, redactor)
|
|
713
|
+
? { body: serializeTextBody(config.data, contentType, redactor) }
|
|
682
714
|
: {})
|
|
683
715
|
}
|
|
684
716
|
};
|
|
@@ -703,6 +735,7 @@ export let recordHttpTraceFromResponse = (response: AxiosResponse) => {
|
|
|
703
735
|
if (!draft) return response;
|
|
704
736
|
clearTraceDraft(config, draft);
|
|
705
737
|
|
|
738
|
+
let redactor = new AuthConfigSecretRedactor(draft.context._getAuthConfigForRedaction());
|
|
706
739
|
let contentType = getContentType(response.headers);
|
|
707
740
|
draft.context.recordHttpTrace({
|
|
708
741
|
startedAt: draft.startedAt,
|
|
@@ -711,11 +744,11 @@ export let recordHttpTraceFromResponse = (response: AxiosResponse) => {
|
|
|
711
744
|
response: {
|
|
712
745
|
status: response.status,
|
|
713
746
|
...(response.statusText ? { statusText: response.statusText } : {}),
|
|
714
|
-
...(sanitizeHeaders(response.headers)
|
|
715
|
-
? { headers: sanitizeHeaders(response.headers) }
|
|
747
|
+
...(sanitizeHeaders(response.headers, redactor)
|
|
748
|
+
? { headers: sanitizeHeaders(response.headers, redactor) }
|
|
716
749
|
: {}),
|
|
717
|
-
...(serializeTextBody(response.data, contentType)
|
|
718
|
-
? { body: serializeTextBody(response.data, contentType) }
|
|
750
|
+
...(serializeTextBody(response.data, contentType, redactor)
|
|
751
|
+
? { body: serializeTextBody(response.data, contentType, redactor) }
|
|
719
752
|
: {})
|
|
720
753
|
}
|
|
721
754
|
});
|
|
@@ -729,6 +762,7 @@ export let recordHttpTraceFromError = (error: AxiosError) => {
|
|
|
729
762
|
if (!draft) return;
|
|
730
763
|
clearTraceDraft(config, draft);
|
|
731
764
|
|
|
765
|
+
let redactor = new AuthConfigSecretRedactor(draft.context._getAuthConfigForRedaction());
|
|
732
766
|
let responseContentType = getContentType(error.response?.headers);
|
|
733
767
|
draft.context.recordHttpTrace({
|
|
734
768
|
startedAt: draft.startedAt,
|
|
@@ -739,11 +773,11 @@ export let recordHttpTraceFromError = (error: AxiosError) => {
|
|
|
739
773
|
response: {
|
|
740
774
|
status: error.response.status,
|
|
741
775
|
...(error.response.statusText ? { statusText: error.response.statusText } : {}),
|
|
742
|
-
...(sanitizeHeaders(error.response.headers)
|
|
743
|
-
? { headers: sanitizeHeaders(error.response.headers) }
|
|
776
|
+
...(sanitizeHeaders(error.response.headers, redactor)
|
|
777
|
+
? { headers: sanitizeHeaders(error.response.headers, redactor) }
|
|
744
778
|
: {}),
|
|
745
|
-
...(serializeTextBody(error.response.data, responseContentType)
|
|
746
|
-
? { body: serializeTextBody(error.response.data, responseContentType) }
|
|
779
|
+
...(serializeTextBody(error.response.data, responseContentType, redactor)
|
|
780
|
+
? { body: serializeTextBody(error.response.data, responseContentType, redactor) }
|
|
747
781
|
: {})
|
|
748
782
|
}
|
|
749
783
|
}
|
package/src/context/context.ts
CHANGED
|
@@ -1,33 +1,104 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import PQueue from 'p-queue';
|
|
3
|
+
import {
|
|
4
|
+
createBase64Attachment,
|
|
5
|
+
createUrlAttachment,
|
|
6
|
+
type SlateAttachment
|
|
7
|
+
} from '../action/attachment';
|
|
8
|
+
import { type SlateLiveInvocationInfo, uploadAttachmentDirect } from '../action/directUpload';
|
|
1
9
|
import type { SlateHttpTrace } from '../axios/trace';
|
|
2
10
|
import type { SlateLogger, SlateLogMessageInput } from '../logger';
|
|
3
11
|
import type { SlateSpecification } from '../specification/specification';
|
|
4
12
|
|
|
5
|
-
export
|
|
6
|
-
|
|
13
|
+
export type { SlateLiveInvocationInfo };
|
|
14
|
+
|
|
15
|
+
export type SlateAddAttachmentContent =
|
|
16
|
+
| Buffer
|
|
17
|
+
| Uint8Array
|
|
18
|
+
| ArrayBuffer
|
|
19
|
+
| ReadableStream<Uint8Array>
|
|
20
|
+
| NodeJS.ReadableStream
|
|
21
|
+
| Response;
|
|
22
|
+
|
|
23
|
+
export type SlateAddAttachmentInput =
|
|
24
|
+
| {
|
|
25
|
+
type: 'url';
|
|
26
|
+
url: string | URL;
|
|
27
|
+
mimeType?: string;
|
|
28
|
+
filename?: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
query?: Record<string, string>;
|
|
31
|
+
refreshReference?: unknown;
|
|
32
|
+
refreshAt?: string;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
type: 'content';
|
|
36
|
+
content: SlateAddAttachmentContent;
|
|
37
|
+
mimeType?: string;
|
|
38
|
+
filename?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let isBufferLike = (value: unknown): value is Buffer | Uint8Array | ArrayBuffer =>
|
|
42
|
+
value instanceof Uint8Array || value instanceof ArrayBuffer;
|
|
43
|
+
|
|
44
|
+
let toUint8Array = (value: Buffer | Uint8Array | ArrayBuffer) =>
|
|
45
|
+
value instanceof ArrayBuffer ? new Uint8Array(value) : value;
|
|
46
|
+
|
|
47
|
+
let isReadableStream = (value: unknown): value is ReadableStream<Uint8Array> =>
|
|
48
|
+
typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;
|
|
49
|
+
|
|
50
|
+
let isNodeReadable = (value: unknown): value is NodeJS.ReadableStream =>
|
|
51
|
+
!!value &&
|
|
52
|
+
typeof value === 'object' &&
|
|
53
|
+
typeof (value as any).pipe === 'function' &&
|
|
54
|
+
typeof (value as any).on === 'function';
|
|
55
|
+
|
|
56
|
+
let isResponseLike = (value: unknown): value is Response =>
|
|
57
|
+
typeof Response !== 'undefined' && value instanceof Response;
|
|
58
|
+
|
|
59
|
+
let readStreamToBuffer = async (stream: ReadableStream<Uint8Array>): Promise<Buffer> => {
|
|
60
|
+
let reader = stream.getReader();
|
|
61
|
+
let chunks: Uint8Array[] = [];
|
|
62
|
+
|
|
63
|
+
while (true) {
|
|
64
|
+
let { done, value } = await reader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
if (value) chunks.push(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Buffer.concat(chunks);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
interface NormalizedStreamInput {
|
|
73
|
+
stream: ReadableStream<Uint8Array>;
|
|
74
|
+
mimeType?: string;
|
|
75
|
+
filename?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class SlatePublicContext<InputType extends {}> {
|
|
7
79
|
#input: InputType;
|
|
8
|
-
#auth: AuthType;
|
|
9
80
|
#httpTraces: SlateHttpTrace[] = [];
|
|
10
81
|
|
|
82
|
+
#liveInvocation: SlateLiveInvocationInfo | null;
|
|
83
|
+
#attachmentsDisabled = false;
|
|
84
|
+
#attachments: SlateAttachment[] = [];
|
|
85
|
+
#pendingUploads: Promise<void>[] = [];
|
|
86
|
+
#uploadQueue = new PQueue({ concurrency: 10 });
|
|
87
|
+
|
|
11
88
|
constructor(
|
|
12
|
-
config: ConfigType,
|
|
13
89
|
input: InputType,
|
|
14
|
-
|
|
15
|
-
private readonly
|
|
16
|
-
|
|
90
|
+
private readonly spec: SlateSpecification<any, any>,
|
|
91
|
+
private readonly logger: SlateLogger,
|
|
92
|
+
liveInvocation: SlateLiveInvocationInfo | null = null
|
|
17
93
|
) {
|
|
18
|
-
this.#config = config;
|
|
19
94
|
this.#input = input;
|
|
20
|
-
this.#
|
|
95
|
+
this.#liveInvocation = liveInvocation;
|
|
21
96
|
}
|
|
22
97
|
|
|
23
98
|
get specification() {
|
|
24
99
|
return this.spec;
|
|
25
100
|
}
|
|
26
101
|
|
|
27
|
-
get config() {
|
|
28
|
-
return Object.freeze(this.#config);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
102
|
get input() {
|
|
32
103
|
return Object.freeze(this.#input);
|
|
33
104
|
}
|
|
@@ -56,10 +127,6 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
|
|
|
56
127
|
: never;
|
|
57
128
|
}
|
|
58
129
|
|
|
59
|
-
get auth() {
|
|
60
|
-
return Object.freeze(this.#auth);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
130
|
recordHttpTrace(trace: SlateHttpTrace) {
|
|
64
131
|
this.#httpTraces.push(trace);
|
|
65
132
|
}
|
|
@@ -100,4 +167,142 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
|
|
|
100
167
|
progress(message: SlateLogMessageInput) {
|
|
101
168
|
this.logger.progress(message);
|
|
102
169
|
}
|
|
170
|
+
|
|
171
|
+
async addAttachment(input: SlateAddAttachmentInput): Promise<void> {
|
|
172
|
+
if (input.type === 'url') {
|
|
173
|
+
this.#attachments.push(
|
|
174
|
+
createUrlAttachment(input.url.toString(), {
|
|
175
|
+
mimeType: input.mimeType,
|
|
176
|
+
headers: input.headers,
|
|
177
|
+
query: input.query,
|
|
178
|
+
refreshReference: input.refreshReference,
|
|
179
|
+
refreshAt: input.refreshAt
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let content = input.content;
|
|
186
|
+
|
|
187
|
+
if (isBufferLike(content)) {
|
|
188
|
+
let bytes = toUint8Array(content);
|
|
189
|
+
this.#attachments.push(
|
|
190
|
+
createBase64Attachment(Buffer.from(bytes).toString('base64'), input.mimeType)
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let normalized = await this.#normalizeStreamInput(content, {
|
|
196
|
+
mimeType: input.mimeType,
|
|
197
|
+
filename: input.filename
|
|
198
|
+
});
|
|
199
|
+
if (!normalized) return;
|
|
200
|
+
|
|
201
|
+
if (!this.#liveInvocation || this.#attachmentsDisabled) {
|
|
202
|
+
let buffer = await readStreamToBuffer(normalized.stream);
|
|
203
|
+
this.#attachments.push(
|
|
204
|
+
createBase64Attachment(buffer.toString('base64'), normalized.mimeType)
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let live = this.#liveInvocation;
|
|
210
|
+
let task = this.#uploadQueue
|
|
211
|
+
.add(() =>
|
|
212
|
+
uploadAttachmentDirect({
|
|
213
|
+
live,
|
|
214
|
+
mimeType: normalized.mimeType,
|
|
215
|
+
filename: normalized.filename,
|
|
216
|
+
body: normalized.stream
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
.then(attachment => {
|
|
220
|
+
this.#attachments.push(attachment);
|
|
221
|
+
})
|
|
222
|
+
.catch(err => {
|
|
223
|
+
this.#attachmentsDisabled = true;
|
|
224
|
+
this.warn({
|
|
225
|
+
message: `Attachment dropped: direct upload failed (${
|
|
226
|
+
err instanceof Error ? err.message : String(err)
|
|
227
|
+
}).`
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.#pendingUploads.push(task);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async #normalizeStreamInput(
|
|
235
|
+
input: ReadableStream<Uint8Array> | NodeJS.ReadableStream | Response,
|
|
236
|
+
opts: { mimeType?: string; filename?: string }
|
|
237
|
+
): Promise<NormalizedStreamInput | null> {
|
|
238
|
+
if (isResponseLike(input)) {
|
|
239
|
+
if (!input.body) {
|
|
240
|
+
this.warn({ message: 'Attachment dropped: response has no body.' });
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
stream: input.body,
|
|
245
|
+
mimeType: opts.mimeType ?? input.headers.get('content-type') ?? undefined,
|
|
246
|
+
filename: opts.filename
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (isReadableStream(input)) {
|
|
251
|
+
return { stream: input, mimeType: opts.mimeType, filename: opts.filename };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (isNodeReadable(input)) {
|
|
255
|
+
return {
|
|
256
|
+
stream: Readable.toWeb(input as any) as unknown as ReadableStream<Uint8Array>,
|
|
257
|
+
mimeType: opts.mimeType,
|
|
258
|
+
filename: opts.filename
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.warn({ message: 'Attachment dropped: unsupported attachment input type.' });
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async _finalizeAttachments(): Promise<SlateAttachment[]> {
|
|
267
|
+
await Promise.allSettled(this.#pendingUploads);
|
|
268
|
+
return this.#attachments;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_getAuthConfigForRedaction(): Record<string, unknown> | undefined {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export class SlateContext<
|
|
277
|
+
ConfigType extends {},
|
|
278
|
+
AuthType extends {},
|
|
279
|
+
InputType extends {}
|
|
280
|
+
> extends SlatePublicContext<InputType> {
|
|
281
|
+
#config: ConfigType;
|
|
282
|
+
#auth: AuthType;
|
|
283
|
+
|
|
284
|
+
constructor(
|
|
285
|
+
config: ConfigType,
|
|
286
|
+
input: InputType,
|
|
287
|
+
auth: AuthType,
|
|
288
|
+
spec: SlateSpecification<ConfigType, AuthType>,
|
|
289
|
+
logger: SlateLogger,
|
|
290
|
+
liveInvocation: SlateLiveInvocationInfo | null = null
|
|
291
|
+
) {
|
|
292
|
+
super(input, spec, logger, liveInvocation);
|
|
293
|
+
this.#config = config;
|
|
294
|
+
this.#auth = auth;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get config() {
|
|
298
|
+
return Object.freeze(this.#config);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
get auth() {
|
|
302
|
+
return Object.freeze(this.#auth);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
override _getAuthConfigForRedaction(): Record<string, unknown> | undefined {
|
|
306
|
+
return this.#auth as Record<string, unknown>;
|
|
307
|
+
}
|
|
103
308
|
}
|
package/src/context/hook.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
import type { SlateContext } from './context';
|
|
2
|
+
import type { SlateContext, SlatePublicContext } from './context';
|
|
3
3
|
|
|
4
4
|
let slateContextStorageKey = Symbol.for('slates.provider.context.asyncLocalStorage');
|
|
5
5
|
let globalWithSlateContext = globalThis as typeof globalThis & {
|
|
6
|
-
[key: symbol]: AsyncLocalStorage<
|
|
6
|
+
[key: symbol]: AsyncLocalStorage<SlatePublicContext<any>> | undefined;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
let asyncLocalStorage =
|
|
10
10
|
globalWithSlateContext[slateContextStorageKey] ??
|
|
11
|
-
new AsyncLocalStorage<
|
|
11
|
+
new AsyncLocalStorage<SlatePublicContext<any>>();
|
|
12
12
|
|
|
13
13
|
globalWithSlateContext[slateContextStorageKey] = asyncLocalStorage;
|
|
14
14
|
|
|
15
|
-
export let runWithContext = <
|
|
16
|
-
|
|
17
|
-
AuthType extends {},
|
|
18
|
-
InputType extends {},
|
|
19
|
-
RV
|
|
20
|
-
>(
|
|
21
|
-
context: SlateContext<ConfigType, AuthType, InputType>,
|
|
15
|
+
export let runWithContext = <InputType extends {}, RV>(
|
|
16
|
+
context: SlatePublicContext<InputType>,
|
|
22
17
|
fn: () => Promise<RV>
|
|
23
18
|
): Promise<RV> => {
|
|
24
19
|
return asyncLocalStorage.run(context, fn);
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SlateTool, SlateTrigger } from '../action';
|
|
2
|
+
import type { SlateAdapter } from '../adapter';
|
|
3
|
+
import { SlateDeclarationError } from '../error';
|
|
2
4
|
import type { SlateSpecification } from './specification';
|
|
3
5
|
|
|
4
6
|
export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
@@ -7,15 +9,45 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
7
9
|
private readonly _actions: (
|
|
8
10
|
| SlateTrigger<ConfigType, AuthType, any, any>
|
|
9
11
|
| SlateTool<ConfigType, AuthType, any, any>
|
|
10
|
-
)[]
|
|
12
|
+
)[],
|
|
13
|
+
private readonly _adapters: SlateAdapter<ConfigType, AuthType>[]
|
|
11
14
|
) {}
|
|
12
15
|
|
|
13
16
|
static create<ConfigType extends {}, AuthType extends {}>(params: {
|
|
14
17
|
spec: SlateSpecification<ConfigType, AuthType>;
|
|
15
18
|
triggers: SlateTrigger<ConfigType, AuthType, any, any>[];
|
|
16
19
|
tools: SlateTool<ConfigType, AuthType, any, any>[];
|
|
20
|
+
adapters?: SlateAdapter<ConfigType, AuthType>[];
|
|
17
21
|
}) {
|
|
18
|
-
|
|
22
|
+
let adapters = params.adapters ?? [];
|
|
23
|
+
let actions = [...params.triggers, ...params.tools];
|
|
24
|
+
let seenAdapterIds = new Set<string>();
|
|
25
|
+
let seenActionKeys = new Map(actions.map(action => [action.key, action]));
|
|
26
|
+
|
|
27
|
+
for (let adapter of adapters) {
|
|
28
|
+
if (seenAdapterIds.has(adapter.id)) {
|
|
29
|
+
throw new SlateDeclarationError(
|
|
30
|
+
`Adapter "${adapter.id}" is registered more than once`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
seenAdapterIds.add(adapter.id);
|
|
34
|
+
|
|
35
|
+
for (let action of adapter.actions) {
|
|
36
|
+
let existing = seenActionKeys.get(action.key);
|
|
37
|
+
if (existing && existing !== action) {
|
|
38
|
+
throw new SlateDeclarationError(
|
|
39
|
+
`Adapter "${adapter.id}" action "${action.key}" conflicts with another action`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!existing) {
|
|
44
|
+
actions.push(action);
|
|
45
|
+
seenActionKeys.set(action.key, action);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new Slate(params.spec, actions, adapters);
|
|
19
51
|
}
|
|
20
52
|
|
|
21
53
|
get spec() {
|
|
@@ -25,4 +57,8 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
25
57
|
get actions() {
|
|
26
58
|
return this._actions;
|
|
27
59
|
}
|
|
60
|
+
|
|
61
|
+
get adapters() {
|
|
62
|
+
return this._adapters;
|
|
63
|
+
}
|
|
28
64
|
}
|