@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.
- package/dist/index.cjs +639 -21
- package/dist/index.d.cts +290 -29
- package/dist/index.d.ts +290 -29
- package/dist/index.module.js +606 -18
- package/package.json +2 -2
- package/src/action/action.ts +64 -2
- package/src/action/builder.test.ts +136 -0
- package/src/action/builder.ts +73 -13
- package/src/action/index.ts +2 -1
- package/src/action/publicTool.ts +36 -0
- package/src/action/tool.ts +15 -3
- package/src/action/trigger.ts +9 -1
- 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/oauth.test.ts +83 -0
- package/src/auth/oauth.ts +150 -0
- package/src/auth/types.ts +22 -6
- package/src/axios/http.test.ts +83 -0
- package/src/axios/index.ts +59 -1
- package/src/axios/trace.ts +3 -3
- package/src/context/context.ts +37 -14
- package/src/context/hook.ts +5 -10
- package/src/crypto/hmac.test.ts +40 -0
- package/src/crypto/hmac.ts +32 -0
- package/src/crypto/index.ts +1 -0
- package/src/error/api.test.ts +114 -0
- package/src/error/api.ts +236 -0
- package/src/error/index.ts +1 -0
- package/src/file/index.test.ts +36 -0
- package/src/file/index.ts +27 -0
- package/src/index.ts +5 -0
- package/src/object/index.test.ts +38 -0
- package/src/object/index.ts +12 -0
- package/src/specification/slate.ts +38 -2
- package/src/webhook/graph.test.ts +37 -0
- package/src/webhook/graph.ts +22 -0
- package/src/webhook/index.ts +2 -0
- package/src/webhook/meta.test.ts +52 -0
- 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: {
|
|
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: {
|
|
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: {
|
|
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
|
+
});
|
package/src/axios/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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();
|
package/src/axios/trace.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
|
2
|
-
import type {
|
|
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:
|
|
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:
|
|
667
|
+
context: SlatePublicContext<any>
|
|
668
668
|
) => {
|
|
669
669
|
let traceAwareConfig = config as TraceAwareAxiosRequestConfig;
|
|
670
670
|
let contentType = getContentType(config.headers);
|
package/src/context/context.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
52
|
-
return Object.freeze(
|
|
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
|
+
}
|
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);
|
|
@@ -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
|
+
});
|