@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.
@@ -1,5 +1,6 @@
1
1
  import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
2
- import type { SlateContext } from '../context';
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: SlateContext<any, any, any>;
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
- let result = text;
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) => truncateText(sanitizeFreeText(value));
412
+ let sanitizeScalar = (value: string, redactor?: AuthConfigSecretRedactor) =>
413
+ truncateText(sanitizeFreeText(value, redactor));
410
414
 
411
- let redactStructuredValue = (value: unknown, depth = 0): unknown => {
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) ? REDACTED_VALUE : redactStructuredValue(entry, depth + 1)
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(JSON.stringify(redactStructuredValue(value)), 'application/json');
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, _rawValue: string) =>
542
- isUrlFormSecretKeyName(key) ? `${key}=${REDACTED_VALUE}` : match
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
- return value.replace(
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(([key, value]) => [key.toLowerCase(), sanitizeScalar(value).text] as [string, string])
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: SlateContext<any, any, any>
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) ? { headers: sanitizeHeaders(config.headers) } : {}),
680
- ...(serializeTextBody(config.data, contentType)
681
- ? { body: serializeTextBody(config.data, contentType) }
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
  }
@@ -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 class SlateContext<ConfigType extends {}, AuthType extends {}, InputType extends {}> {
6
- #config: ConfigType;
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
- auth: AuthType,
15
- private readonly spec: SlateSpecification<ConfigType, AuthType>,
16
- private readonly logger: SlateLogger
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.#auth = auth;
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
  }
@@ -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<SlateContext<any, any, any>> | undefined;
6
+ [key: symbol]: AsyncLocalStorage<SlatePublicContext<any>> | undefined;
7
7
  };
8
8
 
9
9
  let asyncLocalStorage =
10
10
  globalWithSlateContext[slateContextStorageKey] ??
11
- new AsyncLocalStorage<SlateContext<any, any, any>>();
11
+ new AsyncLocalStorage<SlatePublicContext<any>>();
12
12
 
13
13
  globalWithSlateContext[slateContextStorageKey] = asyncLocalStorage;
14
14
 
15
- export let runWithContext = <
16
- ConfigType extends {},
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,5 +1,7 @@
1
1
  export * from './action';
2
2
  export * from './action/attachment';
3
+ export * from './action/directUpload';
4
+ export * from './adapter';
3
5
  export * from './auth';
4
6
  export * from './axios';
5
7
  export * from './config';
@@ -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
- return new Slate(params.spec, [...params.triggers, ...params.tools]);
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
  }