@slates/provider 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (125) hide show
  1. package/dist/index.cjs +2885 -2
  2. package/dist/index.d.cts +1019 -0
  3. package/dist/index.d.ts +1019 -9
  4. package/dist/index.module.js +2775 -2
  5. package/package.json +9 -6
  6. package/src/action/action.ts +92 -4
  7. package/src/action/attachment.ts +87 -0
  8. package/src/action/builder.test.ts +136 -0
  9. package/src/action/builder.ts +113 -15
  10. package/src/action/directUpload.test.ts +70 -0
  11. package/src/action/directUpload.ts +105 -0
  12. package/src/action/getFileUrlTool.ts +41 -0
  13. package/src/action/index.ts +4 -2
  14. package/src/action/publicTool.ts +36 -0
  15. package/src/action/scopes.test.ts +76 -0
  16. package/src/action/scopes.ts +103 -0
  17. package/src/action/tool.ts +22 -6
  18. package/src/action/trigger.ts +19 -11
  19. package/src/adapter/adapter.ts +38 -0
  20. package/src/adapter/index.test.ts +322 -0
  21. package/src/adapter/index.ts +2 -0
  22. package/src/adapter/spec.ts +154 -0
  23. package/src/auth/auth.ts +3 -3
  24. package/src/auth/index.ts +2 -0
  25. package/src/auth/oauth.test.ts +83 -0
  26. package/src/auth/oauth.ts +150 -0
  27. package/src/auth/redact.test.ts +103 -0
  28. package/src/auth/redact.ts +111 -0
  29. package/src/auth/types.ts +72 -13
  30. package/src/axios/http.test.ts +83 -0
  31. package/src/axios/index.ts +116 -10
  32. package/src/axios/trace.test.ts +377 -0
  33. package/src/axios/trace.ts +803 -0
  34. package/src/config/config.ts +14 -3
  35. package/src/context/context.ts +264 -17
  36. package/src/context/hook.test.ts +46 -0
  37. package/src/context/hook.ts +15 -6
  38. package/src/context/index.ts +1 -0
  39. package/src/context/pQueue.ts +4 -0
  40. package/src/crypto/hmac.test.ts +40 -0
  41. package/src/crypto/hmac.ts +32 -0
  42. package/src/crypto/index.ts +1 -0
  43. package/src/docs.ts +22 -0
  44. package/src/error/api.test.ts +114 -0
  45. package/src/error/api.ts +236 -0
  46. package/src/error/axios.ts +220 -0
  47. package/src/error/base.test.ts +178 -0
  48. package/src/error/base.ts +108 -2
  49. package/src/error/declaration.ts +7 -2
  50. package/src/error/defaults.ts +50 -0
  51. package/src/error/index.ts +4 -0
  52. package/src/error/service.ts +170 -0
  53. package/src/error/types.ts +75 -0
  54. package/src/error/utils.ts +157 -0
  55. package/src/file/index.test.ts +36 -0
  56. package/src/file/index.ts +27 -0
  57. package/src/index.ts +8 -0
  58. package/src/logger/logger.test.ts +57 -0
  59. package/src/logger/logger.ts +72 -11
  60. package/src/module/resolveDefaultExport.test.ts +32 -0
  61. package/src/module/resolveDefaultExport.ts +14 -0
  62. package/src/object/index.test.ts +38 -0
  63. package/src/object/index.ts +12 -0
  64. package/src/specification/slate.ts +40 -4
  65. package/src/specification/specification.ts +10 -6
  66. package/src/webhook/graph.test.ts +37 -0
  67. package/src/webhook/graph.ts +22 -0
  68. package/src/webhook/index.ts +2 -0
  69. package/src/webhook/meta.test.ts +52 -0
  70. package/src/webhook/meta.ts +43 -0
  71. package/dist/action/action.d.ts +0 -98
  72. package/dist/action/action.d.ts.map +0 -1
  73. package/dist/action/builder.d.ts +0 -27
  74. package/dist/action/builder.d.ts.map +0 -1
  75. package/dist/action/index.d.ts +0 -5
  76. package/dist/action/index.d.ts.map +0 -1
  77. package/dist/action/tool.d.ts +0 -14
  78. package/dist/action/tool.d.ts.map +0 -1
  79. package/dist/action/trigger.d.ts +0 -32
  80. package/dist/action/trigger.d.ts.map +0 -1
  81. package/dist/auth/auth.d.ts +0 -30
  82. package/dist/auth/auth.d.ts.map +0 -1
  83. package/dist/auth/index.d.ts +0 -3
  84. package/dist/auth/index.d.ts.map +0 -1
  85. package/dist/auth/types.d.ts +0 -148
  86. package/dist/auth/types.d.ts.map +0 -1
  87. package/dist/axios/index.d.ts +0 -4
  88. package/dist/axios/index.d.ts.map +0 -1
  89. package/dist/config/config.d.ts +0 -26
  90. package/dist/config/config.d.ts.map +0 -1
  91. package/dist/config/index.d.ts +0 -2
  92. package/dist/config/index.d.ts.map +0 -1
  93. package/dist/context/context.d.ts +0 -20
  94. package/dist/context/context.d.ts.map +0 -1
  95. package/dist/context/hook.d.ts +0 -4
  96. package/dist/context/hook.d.ts.map +0 -1
  97. package/dist/context/index.d.ts +0 -2
  98. package/dist/context/index.d.ts.map +0 -1
  99. package/dist/error/base.d.ts +0 -5
  100. package/dist/error/base.d.ts.map +0 -1
  101. package/dist/error/declaration.d.ts +0 -6
  102. package/dist/error/declaration.d.ts.map +0 -1
  103. package/dist/error/index.d.ts +0 -3
  104. package/dist/error/index.d.ts.map +0 -1
  105. package/dist/index.cjs.map +0 -1
  106. package/dist/index.d.ts.map +0 -1
  107. package/dist/index.modern.js +0 -2
  108. package/dist/index.modern.js.map +0 -1
  109. package/dist/index.module.js.map +0 -1
  110. package/dist/index.umd.js +0 -2
  111. package/dist/index.umd.js.map +0 -1
  112. package/dist/logger/index.d.ts +0 -2
  113. package/dist/logger/index.d.ts.map +0 -1
  114. package/dist/logger/logger.d.ts +0 -20
  115. package/dist/logger/logger.d.ts.map +0 -1
  116. package/dist/specification/index.d.ts +0 -3
  117. package/dist/specification/index.d.ts.map +0 -1
  118. package/dist/specification/slate.d.ts +0 -15
  119. package/dist/specification/slate.d.ts.map +0 -1
  120. package/dist/specification/specification.d.ts +0 -30
  121. package/dist/specification/specification.d.ts.map +0 -1
  122. package/dist/specification/zero.d.ts +0 -13
  123. package/dist/specification/zero.d.ts.map +0 -1
  124. package/dist/tokens.d.ts +0 -34
  125. package/dist/tokens.d.ts.map +0 -1
@@ -1,12 +1,14 @@
1
- import z from 'zod';
1
+ import type z from 'zod';
2
+ import type { SlateConfigDocsReference } from '../docs';
2
3
 
3
4
  export class SlateConfig<ConfigType extends {}> {
4
5
  #configSchema: z.ZodType<ConfigType>;
6
+ #docs: SlateConfigDocsReference[] | undefined;
5
7
  #configChanged:
6
8
  | ((params: {
7
9
  previousConfig: ConfigType | null;
8
10
  newConfig: ConfigType;
9
- }) => { config: ConfigType | undefined } | void)
11
+ }) => { config: ConfigType | undefined } | undefined)
10
12
  | null = null;
11
13
  #getDefaultConfig: (() => ConfigType) | null = null;
12
14
 
@@ -25,11 +27,16 @@ export class SlateConfig<ConfigType extends {}> {
25
27
  return this as any as SlateConfig<NewConfigType>;
26
28
  }
27
29
 
30
+ docs(docs: SlateConfigDocsReference[]): SlateConfig<ConfigType> {
31
+ this.#docs = docs;
32
+ return this;
33
+ }
34
+
28
35
  onConfigChanged(
29
36
  handler: (params: {
30
37
  previousConfig: ConfigType | null;
31
38
  newConfig: ConfigType;
32
- }) => { config: ConfigType | undefined } | void
39
+ }) => { config: ConfigType | undefined } | undefined
33
40
  ): SlateConfig<ConfigType> {
34
41
  this.#configChanged = handler;
35
42
  return this;
@@ -44,6 +51,10 @@ export class SlateConfig<ConfigType extends {}> {
44
51
  return this.#configSchema;
45
52
  }
46
53
 
54
+ get docsReferences() {
55
+ return this.#docs;
56
+ }
57
+
47
58
  get handlers() {
48
59
  return {
49
60
  configChanged: this.#configChanged,
@@ -1,31 +1,111 @@
1
- import { SlateLogger, SlateLogMessageInput } from '../logger';
2
- import { SlateSpecification } from '../specification/specification';
1
+ import { Readable } from 'node:stream';
2
+ import {
3
+ createBase64Attachment,
4
+ createUrlAttachment,
5
+ type SlateAttachment
6
+ } from '../action/attachment';
7
+ import { type SlateLiveInvocationInfo, uploadAttachmentDirect } from '../action/directUpload';
8
+ import type { SlateHttpTrace } from '../axios/trace';
9
+ import type { SlateLogger, SlateLogMessageInput } from '../logger';
10
+ import type { SlateSpecification } from '../specification/specification';
11
+ import { PQueue } from './pQueue';
3
12
 
4
- export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType extends {}> {
5
- #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 {}> {
6
79
  #input: InputType;
7
- #auth: AuthType;
80
+ #httpTraces: SlateHttpTrace[] = [];
81
+
82
+ #liveInvocation: SlateLiveInvocationInfo | null;
83
+ #attachmentsDisabled = false;
84
+ #attachments: SlateAttachment[] = [];
85
+ #pendingUploads: Promise<void>[] = [];
86
+ #uploadQueue?: InstanceType<typeof PQueue>;
87
+
88
+ #getUploadQueue() {
89
+ if (!this.#uploadQueue) {
90
+ this.#uploadQueue = new PQueue({ concurrency: 10 });
91
+ }
92
+ return this.#uploadQueue;
93
+ }
8
94
 
9
95
  constructor(
10
- config: ConfigType,
11
96
  input: InputType,
12
- auth: AuthType,
13
- private readonly spec: SlateSpecification<ConfigType, AuthType>,
14
- private readonly logger: SlateLogger
97
+ private readonly spec: SlateSpecification<any, any>,
98
+ private readonly logger: SlateLogger,
99
+ liveInvocation: SlateLiveInvocationInfo | null = null
15
100
  ) {
16
- this.#config = config;
17
101
  this.#input = input;
18
- this.#auth = auth;
102
+ this.#liveInvocation = liveInvocation;
19
103
  }
20
104
 
21
105
  get specification() {
22
106
  return this.spec;
23
107
  }
24
108
 
25
- get config() {
26
- return Object.freeze(this.#config);
27
- }
28
-
29
109
  get input() {
30
110
  return Object.freeze(this.#input);
31
111
  }
@@ -46,8 +126,37 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
46
126
  : never;
47
127
  }
48
128
 
49
- get auth() {
50
- return Object.freeze(this.#auth);
129
+ get registrationDetails() {
130
+ return Object.freeze(
131
+ (this.#input as any).registrationDetails
132
+ ) as 'registrationDetails' extends keyof InputType
133
+ ? InputType['registrationDetails']
134
+ : never;
135
+ }
136
+
137
+ recordHttpTrace(trace: SlateHttpTrace) {
138
+ this.#httpTraces.push(trace);
139
+ }
140
+
141
+ getHttpTraces() {
142
+ return this.#httpTraces.map(trace => ({
143
+ ...trace,
144
+ request: {
145
+ ...trace.request,
146
+ ...(trace.request.headers ? { headers: { ...trace.request.headers } } : {}),
147
+ ...(trace.request.body ? { body: { ...trace.request.body } } : {})
148
+ },
149
+ ...(trace.response
150
+ ? {
151
+ response: {
152
+ ...trace.response,
153
+ ...(trace.response.headers ? { headers: { ...trace.response.headers } } : {}),
154
+ ...(trace.response.body ? { body: { ...trace.response.body } } : {})
155
+ }
156
+ }
157
+ : {}),
158
+ ...(trace.error ? { error: { ...trace.error } } : {})
159
+ }));
51
160
  }
52
161
 
53
162
  info(message: SlateLogMessageInput) {
@@ -65,4 +174,142 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
65
174
  progress(message: SlateLogMessageInput) {
66
175
  this.logger.progress(message);
67
176
  }
177
+
178
+ async addAttachment(input: SlateAddAttachmentInput): Promise<void> {
179
+ if (input.type === 'url') {
180
+ this.#attachments.push(
181
+ createUrlAttachment(input.url.toString(), {
182
+ mimeType: input.mimeType,
183
+ headers: input.headers,
184
+ query: input.query,
185
+ refreshReference: input.refreshReference,
186
+ refreshAt: input.refreshAt
187
+ })
188
+ );
189
+ return;
190
+ }
191
+
192
+ let content = input.content;
193
+
194
+ if (isBufferLike(content)) {
195
+ let bytes = toUint8Array(content);
196
+ this.#attachments.push(
197
+ createBase64Attachment(Buffer.from(bytes).toString('base64'), input.mimeType)
198
+ );
199
+ return;
200
+ }
201
+
202
+ let normalized = await this.#normalizeStreamInput(content, {
203
+ mimeType: input.mimeType,
204
+ filename: input.filename
205
+ });
206
+ if (!normalized) return;
207
+
208
+ if (!this.#liveInvocation || this.#attachmentsDisabled) {
209
+ let buffer = await readStreamToBuffer(normalized.stream);
210
+ this.#attachments.push(
211
+ createBase64Attachment(buffer.toString('base64'), normalized.mimeType)
212
+ );
213
+ return;
214
+ }
215
+
216
+ let live = this.#liveInvocation;
217
+ let task = this.#getUploadQueue()
218
+ .add(() =>
219
+ uploadAttachmentDirect({
220
+ live,
221
+ mimeType: normalized.mimeType,
222
+ filename: normalized.filename,
223
+ body: normalized.stream
224
+ })
225
+ )
226
+ .then(attachment => {
227
+ this.#attachments.push(attachment);
228
+ })
229
+ .catch(err => {
230
+ this.#attachmentsDisabled = true;
231
+ this.warn({
232
+ message: `Attachment dropped: direct upload failed (${
233
+ err instanceof Error ? err.message : String(err)
234
+ }).`
235
+ });
236
+ });
237
+
238
+ this.#pendingUploads.push(task);
239
+ }
240
+
241
+ async #normalizeStreamInput(
242
+ input: ReadableStream<Uint8Array> | NodeJS.ReadableStream | Response,
243
+ opts: { mimeType?: string; filename?: string }
244
+ ): Promise<NormalizedStreamInput | null> {
245
+ if (isResponseLike(input)) {
246
+ if (!input.body) {
247
+ this.warn({ message: 'Attachment dropped: response has no body.' });
248
+ return null;
249
+ }
250
+ return {
251
+ stream: input.body,
252
+ mimeType: opts.mimeType ?? input.headers.get('content-type') ?? undefined,
253
+ filename: opts.filename
254
+ };
255
+ }
256
+
257
+ if (isReadableStream(input)) {
258
+ return { stream: input, mimeType: opts.mimeType, filename: opts.filename };
259
+ }
260
+
261
+ if (isNodeReadable(input)) {
262
+ return {
263
+ stream: Readable.toWeb(input as any) as unknown as ReadableStream<Uint8Array>,
264
+ mimeType: opts.mimeType,
265
+ filename: opts.filename
266
+ };
267
+ }
268
+
269
+ this.warn({ message: 'Attachment dropped: unsupported attachment input type.' });
270
+ return null;
271
+ }
272
+
273
+ async _finalizeAttachments(): Promise<SlateAttachment[]> {
274
+ await Promise.allSettled(this.#pendingUploads);
275
+ return this.#attachments;
276
+ }
277
+
278
+ _getAuthConfigForRedaction(): Record<string, unknown> | undefined {
279
+ return undefined;
280
+ }
281
+ }
282
+
283
+ export class SlateContext<
284
+ ConfigType extends {},
285
+ AuthType extends {},
286
+ InputType extends {}
287
+ > extends SlatePublicContext<InputType> {
288
+ #config: ConfigType;
289
+ #auth: AuthType;
290
+
291
+ constructor(
292
+ config: ConfigType,
293
+ input: InputType,
294
+ auth: AuthType,
295
+ spec: SlateSpecification<ConfigType, AuthType>,
296
+ logger: SlateLogger,
297
+ liveInvocation: SlateLiveInvocationInfo | null = null
298
+ ) {
299
+ super(input, spec, logger, liveInvocation);
300
+ this.#config = config;
301
+ this.#auth = auth;
302
+ }
303
+
304
+ get config() {
305
+ return Object.freeze(this.#config);
306
+ }
307
+
308
+ get auth() {
309
+ return Object.freeze(this.#auth);
310
+ }
311
+
312
+ override _getAuthConfigForRedaction(): Record<string, unknown> | undefined {
313
+ return this.#auth as Record<string, unknown>;
314
+ }
68
315
  }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SlateContext } from './context';
3
+ import { runWithContext } from './hook';
4
+
5
+ describe('slate context hook', () => {
6
+ it('constructs a context without requiring p-queue at construction time', () => {
7
+ expect(
8
+ () =>
9
+ new SlateContext(
10
+ {},
11
+ {},
12
+ {},
13
+ {} as any,
14
+ {
15
+ debug: () => {},
16
+ error: () => {},
17
+ info: () => {},
18
+ progress: () => {},
19
+ warn: () => {}
20
+ } as any
21
+ )
22
+ ).not.toThrow();
23
+ });
24
+
25
+ it('shares context storage across duplicate module instances', async () => {
26
+ // @ts-expect-error Vite treats the query as a distinct module instance.
27
+ let duplicateHook: typeof import('./hook') = await import('./hook?duplicate-instance');
28
+ let context = new SlateContext(
29
+ {},
30
+ {},
31
+ {},
32
+ {} as any,
33
+ {
34
+ debug: () => {},
35
+ error: () => {},
36
+ info: () => {},
37
+ progress: () => {},
38
+ warn: () => {}
39
+ } as any
40
+ );
41
+
42
+ let result = await runWithContext(context, async () => duplicateHook.getCurrentContext());
43
+
44
+ expect(result).toBe(context);
45
+ });
46
+ });
@@ -1,12 +1,21 @@
1
1
  import { AsyncLocalStorage } from 'async_hooks';
2
- import { SlateContext } from './context';
2
+ import type { SlateContext, SlatePublicContext } from './context';
3
3
 
4
- let asyncLocalStorage = new AsyncLocalStorage<SlateContext<any, any, any>>();
4
+ let slateContextStorageKey = Symbol.for('slates.provider.context.asyncLocalStorage');
5
+ let globalWithSlateContext = globalThis as typeof globalThis & {
6
+ [key: symbol]: AsyncLocalStorage<SlatePublicContext<any>> | undefined;
7
+ };
8
+
9
+ let asyncLocalStorage =
10
+ globalWithSlateContext[slateContextStorageKey] ??
11
+ new AsyncLocalStorage<SlatePublicContext<any>>();
12
+
13
+ globalWithSlateContext[slateContextStorageKey] = asyncLocalStorage;
5
14
 
6
- export let runWithContext = <ConfigType extends {}, AuthType extends {}, InputType extends {}>(
7
- context: SlateContext<ConfigType, AuthType, InputType>,
8
- fn: () => Promise<void>
9
- ) => {
15
+ export let runWithContext = <InputType extends {}, RV>(
16
+ context: SlatePublicContext<InputType>,
17
+ fn: () => Promise<RV>
18
+ ): Promise<RV> => {
10
19
  return asyncLocalStorage.run(context, fn);
11
20
  };
12
21
 
@@ -1 +1,2 @@
1
1
  export * from './context';
2
+ export * from './hook';
@@ -0,0 +1,4 @@
1
+ import PQueueImport from 'p-queue';
2
+ import { resolveDefaultExport } from '../module/resolveDefaultExport';
3
+
4
+ export let PQueue = resolveDefaultExport<typeof PQueueImport>(PQueueImport);
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createHmacSignature, verifyHmacSignature } from './hmac';
3
+
4
+ describe('createHmacSignature', () => {
5
+ it('creates prefixed hexadecimal HMAC signatures', () => {
6
+ expect(
7
+ createHmacSignature({
8
+ secret: 'secret',
9
+ payload: 'payload',
10
+ digest: 'hex',
11
+ prefix: 'v0='
12
+ })
13
+ ).toBe('v0=b82fcb791acec57859b989b430a826488ce2e479fdf92326bd0a2e8375a42ba4');
14
+ });
15
+ });
16
+
17
+ describe('verifyHmacSignature', () => {
18
+ it('verifies prefixed hexadecimal HMAC signatures', () => {
19
+ expect(
20
+ verifyHmacSignature({
21
+ secret: 'secret',
22
+ payload: 'payload',
23
+ signature: 'v0=b82fcb791acec57859b989b430a826488ce2e479fdf92326bd0a2e8375a42ba4',
24
+ digest: 'hex',
25
+ prefix: 'v0='
26
+ })
27
+ ).toBe(true);
28
+ });
29
+
30
+ it('rejects incorrect signatures, including signatures of a different length', () => {
31
+ expect(
32
+ verifyHmacSignature({
33
+ secret: 'secret',
34
+ payload: 'payload',
35
+ signature: 'invalid',
36
+ digest: 'base64'
37
+ })
38
+ ).toBe(false);
39
+ });
40
+ });
@@ -0,0 +1,32 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+
3
+ export interface CreateHmacSignatureOptions {
4
+ secret: string | Uint8Array;
5
+ payload: string | Uint8Array;
6
+ algorithm?: string;
7
+ digest: 'hex' | 'base64';
8
+ prefix?: string;
9
+ }
10
+
11
+ export interface VerifyHmacSignatureOptions extends CreateHmacSignatureOptions {
12
+ signature: string;
13
+ }
14
+
15
+ export let createHmacSignature = ({
16
+ secret,
17
+ payload,
18
+ algorithm = 'sha256',
19
+ digest,
20
+ prefix = ''
21
+ }: CreateHmacSignatureOptions) =>
22
+ `${prefix}${createHmac(algorithm, secret).update(payload).digest(digest)}`;
23
+
24
+ export let verifyHmacSignature = (options: VerifyHmacSignatureOptions) => {
25
+ let expected = createHmacSignature(options);
26
+ let expectedBytes = Buffer.from(expected);
27
+ let signatureBytes = Buffer.from(options.signature);
28
+ let lengthsMatch = signatureBytes.length === expectedBytes.length;
29
+ let comparableSignature = lengthsMatch ? signatureBytes : Buffer.alloc(expectedBytes.length);
30
+
31
+ return timingSafeEqual(comparableSignature, expectedBytes) && lengthsMatch;
32
+ };
@@ -0,0 +1 @@
1
+ export * from './hmac';
package/src/docs.ts ADDED
@@ -0,0 +1,22 @@
1
+ export interface SlateDocsReference {
2
+ name: string;
3
+ url: string;
4
+ }
5
+
6
+ export interface SlateTypedDocsReference<Type extends string> extends SlateDocsReference {
7
+ type?: Type;
8
+ }
9
+
10
+ export type SlateProviderDocsReference = SlateDocsReference;
11
+
12
+ export type SlateConfigDocsReference = SlateTypedDocsReference<'docs.config.general'>;
13
+
14
+ export type SlateActionDocsReference = SlateTypedDocsReference<'docs.action.general'>;
15
+
16
+ export type SlateAuthDocsReference = SlateTypedDocsReference<
17
+ | 'docs.auth.oauth'
18
+ | 'docs.auth.oauth_scopes'
19
+ | 'docs.auth.token'
20
+ | 'docs.auth.service_account'
21
+ | 'docs.auth.custom'
22
+ >;
@@ -0,0 +1,114 @@
1
+ import { ServiceError } from '@lowerdeck/error';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { buildApiServiceError, collectApiErrorDetails, createApiServiceError } from './api';
4
+ import { SlateError } from './base';
5
+
6
+ describe('API service error helpers', () => {
7
+ it('collects unique nested API error details', () => {
8
+ let details: string[] = [];
9
+
10
+ collectApiErrorDetails(
11
+ {
12
+ message: 'Invalid request',
13
+ code: 'bad_request',
14
+ errors: [
15
+ {
16
+ detail: 'Missing name'
17
+ },
18
+ 'Missing name'
19
+ ]
20
+ },
21
+ details
22
+ );
23
+
24
+ expect(details).toEqual(['Invalid request', 'bad_request', 'Missing name']);
25
+ });
26
+
27
+ it('builds provider API ServiceErrors with upstream metadata', () => {
28
+ let parent = new Error('Request failed');
29
+ (parent as any).response = {
30
+ status: 429,
31
+ statusText: 'Too Many Requests',
32
+ data: {
33
+ title: 'Rate limit',
34
+ details: [{ message: 'Retry later' }]
35
+ }
36
+ };
37
+
38
+ let error = buildApiServiceError(parent, {
39
+ providerLabel: 'Example',
40
+ operation: 'list widgets',
41
+ reason: 'example_api_error',
42
+ detailKeys: ['title', 'message'],
43
+ nestedKeys: ['details'],
44
+ extractUpstreamCode: () => 'RATE_LIMITED'
45
+ });
46
+
47
+ expect(error).toBeInstanceOf(ServiceError);
48
+ expect(error.data.message).toBe(
49
+ 'Example API list widgets failed: HTTP 429 Too Many Requests: Rate limit - Retry later'
50
+ );
51
+ expect(error.data.reason).toBe('example_api_error');
52
+ expect(error.data.upstreamStatus).toBe(429);
53
+ expect(error.data.upstreamCode).toBe('RATE_LIMITED');
54
+ });
55
+
56
+ it('recovers the upstream status from interceptor-normalized SlateErrors', () => {
57
+ // Runtime axios instances reject with SlateError, not AxiosError: the
58
+ // response interceptor consumes error.response, so the status only
59
+ // survives at data.upstream.status / data.status.
60
+ let parent = new SlateError({
61
+ code: 'not_found',
62
+ message: 'Method not found.',
63
+ upstream: { status: 404 }
64
+ });
65
+
66
+ let error = buildApiServiceError(parent, {
67
+ providerLabel: 'Example',
68
+ operation: 'search messages',
69
+ reason: 'example_api_error'
70
+ });
71
+
72
+ expect(error.data.upstreamStatus).toBe(404);
73
+ expect(error.data.message).toBe(
74
+ 'Example API search messages failed: HTTP 404: Method not found.'
75
+ );
76
+ });
77
+
78
+ it('allows provider-specific status and message extraction', () => {
79
+ let error = buildApiServiceError(
80
+ {
81
+ status: 503,
82
+ data: {
83
+ message: 'Unavailable'
84
+ }
85
+ },
86
+ {
87
+ providerLabel: 'Example',
88
+ reason: 'example_api_error',
89
+ extractStatus: (input, _response, helpers) =>
90
+ helpers.isRecord(input) && typeof input.status === 'number'
91
+ ? input.status
92
+ : undefined,
93
+ extractMessage: (input, helpers) => {
94
+ if (!helpers.isRecord(input) || !helpers.isRecord(input.data)) return undefined;
95
+ return typeof input.data.message === 'string' ? input.data.message : undefined;
96
+ }
97
+ }
98
+ );
99
+
100
+ expect(error.data.message).toBe('Example API request failed: HTTP 503: Unavailable');
101
+ expect(error.data.upstreamStatus).toBe(503);
102
+ });
103
+
104
+ it('passes through existing ServiceErrors', () => {
105
+ let existing = createApiServiceError('Already normalized');
106
+
107
+ expect(
108
+ buildApiServiceError(existing, {
109
+ providerLabel: 'Example',
110
+ reason: 'example_api_error'
111
+ })
112
+ ).toBe(existing);
113
+ });
114
+ });