@slates/provider 1.0.0-rc.16 → 1.0.0-rc.18

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 (42) hide show
  1. package/dist/index.cjs +639 -21
  2. package/dist/index.d.cts +290 -29
  3. package/dist/index.d.ts +290 -29
  4. package/dist/index.module.js +606 -18
  5. package/package.json +2 -2
  6. package/src/action/action.ts +64 -2
  7. package/src/action/builder.test.ts +136 -0
  8. package/src/action/builder.ts +73 -13
  9. package/src/action/index.ts +2 -1
  10. package/src/action/publicTool.ts +36 -0
  11. package/src/action/tool.ts +15 -3
  12. package/src/action/trigger.ts +9 -1
  13. package/src/adapter/adapter.ts +38 -0
  14. package/src/adapter/index.test.ts +322 -0
  15. package/src/adapter/index.ts +2 -0
  16. package/src/adapter/spec.ts +154 -0
  17. package/src/auth/index.ts +1 -0
  18. package/src/auth/oauth.test.ts +83 -0
  19. package/src/auth/oauth.ts +150 -0
  20. package/src/auth/types.ts +22 -6
  21. package/src/axios/http.test.ts +83 -0
  22. package/src/axios/index.ts +59 -1
  23. package/src/axios/trace.ts +3 -3
  24. package/src/context/context.ts +37 -14
  25. package/src/context/hook.ts +5 -10
  26. package/src/crypto/hmac.test.ts +40 -0
  27. package/src/crypto/hmac.ts +32 -0
  28. package/src/crypto/index.ts +1 -0
  29. package/src/error/api.test.ts +114 -0
  30. package/src/error/api.ts +236 -0
  31. package/src/error/index.ts +1 -0
  32. package/src/file/index.test.ts +36 -0
  33. package/src/file/index.ts +27 -0
  34. package/src/index.ts +5 -0
  35. package/src/object/index.test.ts +38 -0
  36. package/src/object/index.ts +12 -0
  37. package/src/specification/slate.ts +38 -2
  38. package/src/webhook/graph.test.ts +37 -0
  39. package/src/webhook/graph.ts +22 -0
  40. package/src/webhook/index.ts +2 -0
  41. package/src/webhook/meta.test.ts +52 -0
  42. package/src/webhook/meta.ts +43 -0
package/src/auth/types.ts CHANGED
@@ -97,7 +97,7 @@ export type SlateAuthWithToken<InputType extends {}, OutputType extends { token?
97
97
 
98
98
  docs?: SlateAuthDocsReference[];
99
99
 
100
- getOutput: (ctx: { input: InputType }) => Promise<{
100
+ getOutput: (ctx: { input: InputType; config?: SlateAuthCallbackConfig }) => Promise<{
101
101
  output: OutputType;
102
102
  scopes?: string[];
103
103
  }>;
@@ -109,7 +109,12 @@ export type SlateAuthWithToken<InputType extends {}, OutputType extends { token?
109
109
 
110
110
  getDefaultInput?: () => Promise<InputType>;
111
111
 
112
- getProfile?: (ctx: { output: OutputType; input: InputType }) => Promise<{
112
+ getProfile?: (ctx: {
113
+ output: OutputType;
114
+ input: InputType;
115
+ scopes: string[];
116
+ config?: SlateAuthCallbackConfig;
117
+ }) => Promise<{
113
118
  profile: Record<string, any>;
114
119
  }>;
115
120
  };
@@ -124,7 +129,7 @@ export type SlateAuthWithServiceAccount<InputType extends {}, OutputType extends
124
129
 
125
130
  docs?: SlateAuthDocsReference[];
126
131
 
127
- getOutput: (ctx: { input: InputType }) => Promise<{
132
+ getOutput: (ctx: { input: InputType; config?: SlateAuthCallbackConfig }) => Promise<{
128
133
  output: OutputType;
129
134
  scopes?: string[];
130
135
  }>;
@@ -136,7 +141,12 @@ export type SlateAuthWithServiceAccount<InputType extends {}, OutputType extends
136
141
 
137
142
  getDefaultInput?: () => Promise<InputType>;
138
143
 
139
- getProfile?: (ctx: { output: OutputType; input: InputType }) => Promise<{
144
+ getProfile?: (ctx: {
145
+ output: OutputType;
146
+ input: InputType;
147
+ scopes: string[];
148
+ config?: SlateAuthCallbackConfig;
149
+ }) => Promise<{
140
150
  profile: Record<string, any>;
141
151
  }>;
142
152
  };
@@ -151,7 +161,7 @@ export type SlateAuthWithCustomData<InputType extends {}, OutputType extends {}>
151
161
 
152
162
  docs?: SlateAuthDocsReference[];
153
163
 
154
- getOutput: (ctx: { input: InputType }) => Promise<{
164
+ getOutput: (ctx: { input: InputType; config?: SlateAuthCallbackConfig }) => Promise<{
155
165
  output: OutputType;
156
166
  scopes?: string[];
157
167
  }>;
@@ -162,6 +172,7 @@ export type SlateAuthWithCustomData<InputType extends {}, OutputType extends {}>
162
172
  clientId: string;
163
173
  clientSecret: string;
164
174
  scopes: string[];
175
+ config?: SlateAuthCallbackConfig;
165
176
  }) => Promise<{
166
177
  output: OutputType;
167
178
  input?: InputType;
@@ -174,7 +185,12 @@ export type SlateAuthWithCustomData<InputType extends {}, OutputType extends {}>
174
185
 
175
186
  getDefaultInput?: () => Promise<InputType>;
176
187
 
177
- getProfile?: (ctx: { output: OutputType; input: InputType }) => Promise<{
188
+ getProfile?: (ctx: {
189
+ output: OutputType;
190
+ input: InputType;
191
+ scopes: string[];
192
+ config?: SlateAuthCallbackConfig;
193
+ }) => Promise<{
178
194
  profile: Record<string, any>;
179
195
  }>;
180
196
  };
@@ -0,0 +1,83 @@
1
+ import type { AxiosResponse } from 'axios';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createAuthenticatedAxios, requestAxios, requestAxiosData } from './index';
4
+
5
+ describe('createAuthenticatedAxios', () => {
6
+ it('sets auth and JSON headers while preserving caller headers', () => {
7
+ let client = createAuthenticatedAxios({
8
+ baseURL: 'https://example.test',
9
+ authHeader: {
10
+ value: 'Bearer token'
11
+ },
12
+ headers: {
13
+ Accept: 'application/json'
14
+ }
15
+ });
16
+
17
+ expect(client.defaults.baseURL).toBe('https://example.test');
18
+ expect(client.defaults.headers.Authorization).toBe('Bearer token');
19
+ expect(client.defaults.headers['Content-Type']).toBe('application/json');
20
+ expect(client.defaults.headers.Accept).toBe('application/json');
21
+ });
22
+
23
+ it('allows custom auth header names and disabling content type defaults', () => {
24
+ let client = createAuthenticatedAxios({
25
+ authHeader: {
26
+ name: 'X-API-Key',
27
+ value: 'secret'
28
+ },
29
+ contentType: false
30
+ });
31
+
32
+ expect(client.defaults.headers['X-API-Key']).toBe('secret');
33
+ expect(client.defaults.headers['Content-Type']).toBeUndefined();
34
+ });
35
+ });
36
+
37
+ describe('requestAxios helpers', () => {
38
+ it('returns full responses or response data', async () => {
39
+ let response = {
40
+ config: {} as AxiosResponse<{ ok: boolean }>['config'],
41
+ data: { ok: true },
42
+ headers: {},
43
+ status: 200,
44
+ statusText: 'OK'
45
+ };
46
+
47
+ await expect(
48
+ requestAxios(
49
+ 'get thing',
50
+ async () => response,
51
+ error => error
52
+ )
53
+ ).resolves.toBe(response);
54
+ await expect(
55
+ requestAxiosData(
56
+ 'get thing',
57
+ async () => response,
58
+ error => error
59
+ )
60
+ ).resolves.toEqual({ ok: true });
61
+ });
62
+
63
+ it('maps failures with the operation name', async () => {
64
+ let upstream = new Error('upstream failed');
65
+ let mapped = new Error('mapped failure');
66
+ let seenOperation: string | undefined;
67
+
68
+ await expect(
69
+ requestAxiosData(
70
+ 'create thing',
71
+ async () => {
72
+ throw upstream;
73
+ },
74
+ (_error, operation) => {
75
+ seenOperation = operation;
76
+ return mapped;
77
+ }
78
+ )
79
+ ).rejects.toBe(mapped);
80
+
81
+ expect(seenOperation).toBe('create thing');
82
+ });
83
+ });
@@ -1,4 +1,9 @@
1
- import type { AxiosInstance, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
1
+ import type {
2
+ AxiosInstance,
3
+ AxiosResponse,
4
+ CreateAxiosDefaults,
5
+ InternalAxiosRequestConfig
6
+ } from 'axios';
2
7
  import baseAxios, { getAdapter, isAxiosError } from 'axios';
3
8
  import { getCurrentContext } from '../context/hook';
4
9
  import type { SlateAxiosErrorOptions } from '../error';
@@ -13,6 +18,17 @@ export interface SlateAxiosDefaults extends CreateAxiosDefaults {
13
18
  errorMapping?: SlateAxiosErrorOptions;
14
19
  }
15
20
 
21
+ export type SlateAxiosErrorAdapter = (error: unknown, operation: string) => unknown;
22
+
23
+ export interface SlateAuthenticatedAxiosDefaults extends SlateAxiosDefaults {
24
+ authHeader?: {
25
+ name?: string;
26
+ value: string;
27
+ };
28
+ contentType?: string | false;
29
+ errorAdapter?: (error: unknown) => unknown;
30
+ }
31
+
16
32
  let applySlateInterceptors = (
17
33
  instance: AxiosInstance,
18
34
  errorMapping?: SlateAxiosErrorOptions
@@ -73,4 +89,46 @@ export let createAxios = (config?: SlateAxiosDefaults) => {
73
89
  return instance;
74
90
  };
75
91
 
92
+ export let createAuthenticatedAxios = (config: SlateAuthenticatedAxiosDefaults) => {
93
+ let { authHeader, contentType = 'application/json', errorAdapter, ...axiosConfig } = config;
94
+ let instance = createAxios({
95
+ ...axiosConfig,
96
+ headers: {
97
+ ...(contentType === false ? {} : { 'Content-Type': contentType }),
98
+ ...(authHeader ? { [authHeader.name ?? 'Authorization']: authHeader.value } : {}),
99
+ ...axiosConfig.headers
100
+ }
101
+ });
102
+
103
+ if (errorAdapter) {
104
+ instance.interceptors.response.use(
105
+ response => response,
106
+ error => Promise.reject(errorAdapter(error))
107
+ );
108
+ }
109
+
110
+ return instance;
111
+ };
112
+
113
+ export let requestAxios = async <T = unknown>(
114
+ operation: string,
115
+ request: () => Promise<AxiosResponse<T>>,
116
+ errorAdapter: SlateAxiosErrorAdapter
117
+ ) => {
118
+ try {
119
+ return await request();
120
+ } catch (error) {
121
+ throw errorAdapter(error, operation);
122
+ }
123
+ };
124
+
125
+ export let requestAxiosData = async <T = unknown>(
126
+ operation: string,
127
+ request: () => Promise<AxiosResponse<T>>,
128
+ errorAdapter: SlateAxiosErrorAdapter
129
+ ) => {
130
+ let response = await requestAxios(operation, request, errorAdapter);
131
+ return response.data as T;
132
+ };
133
+
76
134
  export let axios = createAxios();
@@ -1,5 +1,5 @@
1
1
  import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
2
- import type { SlateContext } from '../context';
2
+ import type { SlatePublicContext } from '../context';
3
3
 
4
4
  export interface SlateHttpTraceTextBody {
5
5
  contentType?: string;
@@ -29,7 +29,7 @@ export interface SlateHttpTrace {
29
29
  }
30
30
 
31
31
  interface SlateHttpTraceDraft {
32
- context: SlateContext<any, any, any>;
32
+ context: SlatePublicContext<any>;
33
33
  startedAt: string;
34
34
  startedAtMs: number;
35
35
  request: SlateHttpTrace['request'];
@@ -664,7 +664,7 @@ let clearTraceDraft = (
664
664
 
665
665
  export let attachHttpTraceDraft = (
666
666
  config: InternalAxiosRequestConfig,
667
- context: SlateContext<any, any, any>
667
+ context: SlatePublicContext<any>
668
668
  ) => {
669
669
  let traceAwareConfig = config as TraceAwareAxiosRequestConfig;
670
670
  let contentType = getContentType(config.headers);
@@ -2,32 +2,22 @@ import type { SlateHttpTrace } from '../axios/trace';
2
2
  import type { SlateLogger, SlateLogMessageInput } from '../logger';
3
3
  import type { SlateSpecification } from '../specification/specification';
4
4
 
5
- export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType extends {}> {
6
- #config: ConfigType;
5
+ export class SlatePublicContext<InputType extends {}> {
7
6
  #input: InputType;
8
- #auth: AuthType;
9
7
  #httpTraces: SlateHttpTrace[] = [];
10
8
 
11
9
  constructor(
12
- config: ConfigType,
13
10
  input: InputType,
14
- auth: AuthType,
15
- private readonly spec: SlateSpecification<ConfigType, AuthType>,
11
+ private readonly spec: SlateSpecification<any, any>,
16
12
  private readonly logger: SlateLogger
17
13
  ) {
18
- this.#config = config;
19
14
  this.#input = input;
20
- this.#auth = auth;
21
15
  }
22
16
 
23
17
  get specification() {
24
18
  return this.spec;
25
19
  }
26
20
 
27
- get config() {
28
- return Object.freeze(this.#config);
29
- }
30
-
31
21
  get input() {
32
22
  return Object.freeze(this.#input);
33
23
  }
@@ -48,8 +38,12 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
48
38
  : never;
49
39
  }
50
40
 
51
- get auth() {
52
- return Object.freeze(this.#auth);
41
+ get registrationDetails() {
42
+ return Object.freeze(
43
+ (this.#input as any).registrationDetails
44
+ ) as 'registrationDetails' extends keyof InputType
45
+ ? InputType['registrationDetails']
46
+ : never;
53
47
  }
54
48
 
55
49
  recordHttpTrace(trace: SlateHttpTrace) {
@@ -93,3 +87,32 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
93
87
  this.logger.progress(message);
94
88
  }
95
89
  }
90
+
91
+ export class SlateContext<
92
+ ConfigType extends {},
93
+ AuthType extends {},
94
+ InputType extends {}
95
+ > extends SlatePublicContext<InputType> {
96
+ #config: ConfigType;
97
+ #auth: AuthType;
98
+
99
+ constructor(
100
+ config: ConfigType,
101
+ input: InputType,
102
+ auth: AuthType,
103
+ spec: SlateSpecification<ConfigType, AuthType>,
104
+ logger: SlateLogger
105
+ ) {
106
+ super(input, spec, logger);
107
+ this.#config = config;
108
+ this.#auth = auth;
109
+ }
110
+
111
+ get config() {
112
+ return Object.freeze(this.#config);
113
+ }
114
+
115
+ get auth() {
116
+ return Object.freeze(this.#auth);
117
+ }
118
+ }
@@ -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);
@@ -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';
@@ -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
+ });