@slates/provider 1.0.0-rc.15 → 1.0.0-rc.17
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 +414 -5
- package/dist/index.d.cts +190 -1
- package/dist/index.d.ts +190 -1
- package/dist/index.module.js +388 -2
- package/package.json +2 -2
- package/src/action/action.ts +50 -1
- package/src/action/builder.ts +26 -2
- package/src/action/trigger.ts +9 -1
- 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 +31 -6
- package/src/axios/http.test.ts +83 -0
- package/src/axios/index.ts +59 -1
- package/src/context/context.ts +8 -0
- 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 +4 -0
- package/src/object/index.test.ts +38 -0
- package/src/object/index.ts +12 -0
- 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/action/trigger.ts
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
type SlateTriggerPollingHandler,
|
|
9
9
|
type SlateTriggerWebhookAutoRegistrationHandler,
|
|
10
10
|
type SlateTriggerWebhookAutoUnregistrationHandler,
|
|
11
|
-
type SlateTriggerWebhookRequestHandler
|
|
11
|
+
type SlateTriggerWebhookRequestHandler,
|
|
12
|
+
type SlateWebhookHttpOptions
|
|
12
13
|
} from './action';
|
|
13
14
|
import { SlateActionBuilder } from './builder';
|
|
14
15
|
|
|
@@ -27,6 +28,7 @@ export interface SlateTriggerParameters<
|
|
|
27
28
|
polling?: SlatePollingOptions;
|
|
28
29
|
handleEvent: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
|
|
29
30
|
handleRequest?: SlateTriggerWebhookRequestHandler<ConfigType, AuthType, InputType>;
|
|
31
|
+
http?: SlateWebhookHttpOptions;
|
|
30
32
|
pollEvents?: SlateTriggerPollingHandler<ConfigType, AuthType, InputType>;
|
|
31
33
|
autoRegisterWebhook?: SlateTriggerWebhookAutoRegistrationHandler<ConfigType, AuthType>;
|
|
32
34
|
autoUnregisterWebhook?: SlateTriggerWebhookAutoUnregistrationHandler<ConfigType, AuthType>;
|
|
@@ -44,6 +46,7 @@ export class SlateTrigger<
|
|
|
44
46
|
#polling: SlatePollingOptions;
|
|
45
47
|
#handleEvent: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
|
|
46
48
|
#handleRequest?: SlateTriggerWebhookRequestHandler<ConfigType, AuthType, InputType>;
|
|
49
|
+
#http?: SlateWebhookHttpOptions;
|
|
47
50
|
#pollEvents?: SlateTriggerPollingHandler<ConfigType, AuthType, InputType>;
|
|
48
51
|
#autoRegisterWebhook?: SlateTriggerWebhookAutoRegistrationHandler<ConfigType, AuthType>;
|
|
49
52
|
#autoUnregisterWebhook?: SlateTriggerWebhookAutoUnregistrationHandler<ConfigType, AuthType>;
|
|
@@ -62,6 +65,7 @@ export class SlateTrigger<
|
|
|
62
65
|
};
|
|
63
66
|
this.#handleEvent = params.handleEvent;
|
|
64
67
|
this.#handleRequest = params.handleRequest;
|
|
68
|
+
this.#http = params.http;
|
|
65
69
|
this.#pollEvents = params.pollEvents;
|
|
66
70
|
this.#autoRegisterWebhook = params.autoRegisterWebhook;
|
|
67
71
|
this.#autoUnregisterWebhook = params.autoUnregisterWebhook;
|
|
@@ -93,6 +97,10 @@ export class SlateTrigger<
|
|
|
93
97
|
return this.#handleRequest;
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
get http() {
|
|
101
|
+
return this.#http;
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
get pollEvents() {
|
|
97
105
|
return this.#pollEvents;
|
|
98
106
|
}
|
package/src/auth/index.ts
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ServiceError } from '@lowerdeck/error';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { ApiServiceError } from '../error/api';
|
|
4
|
+
import { getOAuthExpiresAtFromExpiresIn, normalizeOAuthTokenResponse } from './oauth';
|
|
5
|
+
|
|
6
|
+
describe('OAuth auth helpers', () => {
|
|
7
|
+
it('converts expires_in seconds to an ISO expiresAt timestamp', () => {
|
|
8
|
+
expect(getOAuthExpiresAtFromExpiresIn(60, { nowMs: 0 })).toBe('1970-01-01T00:01:00.000Z');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('normalizes token responses and preserves omitted refresh tokens', () => {
|
|
12
|
+
expect(
|
|
13
|
+
normalizeOAuthTokenResponse(
|
|
14
|
+
{
|
|
15
|
+
access_token: 'access-token',
|
|
16
|
+
expires_in: '120'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
nowMs: 0,
|
|
20
|
+
previousRefreshToken: 'existing-refresh-token'
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
).toEqual({
|
|
24
|
+
token: 'access-token',
|
|
25
|
+
refreshToken: 'existing-refresh-token',
|
|
26
|
+
expiresAt: '1970-01-01T00:02:00.000Z'
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('supports falsy refresh-token fallback semantics', () => {
|
|
31
|
+
expect(
|
|
32
|
+
normalizeOAuthTokenResponse(
|
|
33
|
+
{
|
|
34
|
+
access_token: 'access-token',
|
|
35
|
+
refresh_token: ''
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
previousRefreshToken: 'existing-refresh-token',
|
|
39
|
+
refreshTokenFallbackMode: 'falsy'
|
|
40
|
+
}
|
|
41
|
+
).refreshToken
|
|
42
|
+
).toBe('existing-refresh-token');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('throws ServiceError for missing access tokens', () => {
|
|
46
|
+
let call = () =>
|
|
47
|
+
normalizeOAuthTokenResponse(
|
|
48
|
+
{
|
|
49
|
+
expires_in: 120
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
providerLabel: 'Example',
|
|
53
|
+
operation: 'token exchange'
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(call).toThrow(ServiceError);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
call();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
expect(error).toBeInstanceOf(ServiceError);
|
|
63
|
+
expect((error as ApiServiceError).data.reason).toBe('oauth_token_response');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('can require a numeric expires_in value', () => {
|
|
68
|
+
expect(() =>
|
|
69
|
+
normalizeOAuthTokenResponse(
|
|
70
|
+
{
|
|
71
|
+
access_token: 'access-token',
|
|
72
|
+
expires_in: '120'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
providerLabel: 'Example',
|
|
76
|
+
operation: 'token exchange',
|
|
77
|
+
required: true,
|
|
78
|
+
expiresInType: 'number'
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
).toThrow(ServiceError);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createApiServiceError } from '../error/api';
|
|
2
|
+
|
|
3
|
+
export type OAuthTokenResponse = {
|
|
4
|
+
access_token?: unknown;
|
|
5
|
+
refresh_token?: unknown;
|
|
6
|
+
expires_in?: unknown;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type OAuthRefreshTokenFallbackMode = 'nullish' | 'falsy';
|
|
11
|
+
|
|
12
|
+
export type OAuthExpiresAtOptions = {
|
|
13
|
+
nowMs?: number;
|
|
14
|
+
providerLabel?: string;
|
|
15
|
+
operation?: string;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
expiresInType?: 'number' | 'numberLike';
|
|
18
|
+
message?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type NormalizeOAuthTokenResponseOptions = OAuthExpiresAtOptions & {
|
|
22
|
+
previousRefreshToken?: string;
|
|
23
|
+
refreshTokenFallbackMode?: OAuthRefreshTokenFallbackMode;
|
|
24
|
+
accessTokenMessage?: string;
|
|
25
|
+
expiresInMessage?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let isOAuthTokenResponse = (value: unknown): value is OAuthTokenResponse =>
|
|
29
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
|
|
31
|
+
let formatOAuthTokenResponseMessage = (
|
|
32
|
+
fieldLabel: string,
|
|
33
|
+
providerLabel = 'OAuth provider',
|
|
34
|
+
operation = 'token response'
|
|
35
|
+
) => `${providerLabel} OAuth ${operation} did not return ${fieldLabel}.`;
|
|
36
|
+
|
|
37
|
+
export let getOAuthExpiresAtFromExpiresIn = (
|
|
38
|
+
expiresIn: unknown,
|
|
39
|
+
options: OAuthExpiresAtOptions = {}
|
|
40
|
+
) => {
|
|
41
|
+
let {
|
|
42
|
+
nowMs = Date.now(),
|
|
43
|
+
providerLabel,
|
|
44
|
+
operation,
|
|
45
|
+
required = false,
|
|
46
|
+
expiresInType = 'numberLike',
|
|
47
|
+
message
|
|
48
|
+
} = options;
|
|
49
|
+
|
|
50
|
+
if (expiresIn === undefined || expiresIn === null || expiresIn === '') {
|
|
51
|
+
if (required) {
|
|
52
|
+
throw createApiServiceError(
|
|
53
|
+
message ??
|
|
54
|
+
formatOAuthTokenResponseMessage(
|
|
55
|
+
'a valid expires_in value',
|
|
56
|
+
providerLabel,
|
|
57
|
+
operation
|
|
58
|
+
),
|
|
59
|
+
{ reason: 'oauth_token_response' }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let seconds =
|
|
67
|
+
typeof expiresIn === 'number'
|
|
68
|
+
? expiresIn
|
|
69
|
+
: expiresInType === 'numberLike' && typeof expiresIn === 'string'
|
|
70
|
+
? Number(expiresIn)
|
|
71
|
+
: Number.NaN;
|
|
72
|
+
|
|
73
|
+
if (!Number.isFinite(seconds)) {
|
|
74
|
+
throw createApiServiceError(
|
|
75
|
+
message ??
|
|
76
|
+
formatOAuthTokenResponseMessage('a valid expires_in value', providerLabel, operation),
|
|
77
|
+
{ reason: 'oauth_token_response' }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Date(nowMs + seconds * 1000).toISOString();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let getRefreshToken = (
|
|
85
|
+
refreshToken: unknown,
|
|
86
|
+
previousRefreshToken: string | undefined,
|
|
87
|
+
fallbackMode: OAuthRefreshTokenFallbackMode
|
|
88
|
+
) => {
|
|
89
|
+
if (typeof refreshToken === 'string') {
|
|
90
|
+
if (fallbackMode === 'falsy' && !refreshToken) {
|
|
91
|
+
return previousRefreshToken;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return refreshToken;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fallbackMode === 'falsy' && !refreshToken) {
|
|
98
|
+
return previousRefreshToken;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return refreshToken === undefined || refreshToken === null
|
|
102
|
+
? previousRefreshToken
|
|
103
|
+
: undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export let normalizeOAuthTokenResponse = (
|
|
107
|
+
data: unknown,
|
|
108
|
+
options: NormalizeOAuthTokenResponseOptions = {}
|
|
109
|
+
) => {
|
|
110
|
+
let {
|
|
111
|
+
providerLabel,
|
|
112
|
+
operation,
|
|
113
|
+
previousRefreshToken,
|
|
114
|
+
refreshTokenFallbackMode = 'nullish',
|
|
115
|
+
accessTokenMessage,
|
|
116
|
+
expiresInMessage,
|
|
117
|
+
...expiresAtOptions
|
|
118
|
+
} = options;
|
|
119
|
+
|
|
120
|
+
if (!isOAuthTokenResponse(data)) {
|
|
121
|
+
throw createApiServiceError(
|
|
122
|
+
accessTokenMessage ??
|
|
123
|
+
formatOAuthTokenResponseMessage('a token response object', providerLabel, operation),
|
|
124
|
+
{ reason: 'oauth_token_response' }
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof data.access_token !== 'string' || !data.access_token) {
|
|
129
|
+
throw createApiServiceError(
|
|
130
|
+
accessTokenMessage ??
|
|
131
|
+
formatOAuthTokenResponseMessage('an access token', providerLabel, operation),
|
|
132
|
+
{ reason: 'oauth_token_response' }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
token: data.access_token,
|
|
138
|
+
refreshToken: getRefreshToken(
|
|
139
|
+
data.refresh_token,
|
|
140
|
+
previousRefreshToken,
|
|
141
|
+
refreshTokenFallbackMode
|
|
142
|
+
),
|
|
143
|
+
expiresAt: getOAuthExpiresAtFromExpiresIn(data.expires_in, {
|
|
144
|
+
...expiresAtOptions,
|
|
145
|
+
providerLabel,
|
|
146
|
+
operation,
|
|
147
|
+
message: expiresInMessage
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
};
|
package/src/auth/types.ts
CHANGED
|
@@ -97,7 +97,10 @@ 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
|
+
output: OutputType;
|
|
102
|
+
scopes?: string[];
|
|
103
|
+
}>;
|
|
101
104
|
|
|
102
105
|
onInputChanged?: (params: {
|
|
103
106
|
previousInput: InputType;
|
|
@@ -106,7 +109,12 @@ export type SlateAuthWithToken<InputType extends {}, OutputType extends { token?
|
|
|
106
109
|
|
|
107
110
|
getDefaultInput?: () => Promise<InputType>;
|
|
108
111
|
|
|
109
|
-
getProfile?: (ctx: {
|
|
112
|
+
getProfile?: (ctx: {
|
|
113
|
+
output: OutputType;
|
|
114
|
+
input: InputType;
|
|
115
|
+
scopes: string[];
|
|
116
|
+
config?: SlateAuthCallbackConfig;
|
|
117
|
+
}) => Promise<{
|
|
110
118
|
profile: Record<string, any>;
|
|
111
119
|
}>;
|
|
112
120
|
};
|
|
@@ -121,7 +129,10 @@ export type SlateAuthWithServiceAccount<InputType extends {}, OutputType extends
|
|
|
121
129
|
|
|
122
130
|
docs?: SlateAuthDocsReference[];
|
|
123
131
|
|
|
124
|
-
getOutput: (ctx: { input: InputType }) => Promise<{
|
|
132
|
+
getOutput: (ctx: { input: InputType; config?: SlateAuthCallbackConfig }) => Promise<{
|
|
133
|
+
output: OutputType;
|
|
134
|
+
scopes?: string[];
|
|
135
|
+
}>;
|
|
125
136
|
|
|
126
137
|
onInputChanged?: (params: {
|
|
127
138
|
previousInput: InputType;
|
|
@@ -130,7 +141,12 @@ export type SlateAuthWithServiceAccount<InputType extends {}, OutputType extends
|
|
|
130
141
|
|
|
131
142
|
getDefaultInput?: () => Promise<InputType>;
|
|
132
143
|
|
|
133
|
-
getProfile?: (ctx: {
|
|
144
|
+
getProfile?: (ctx: {
|
|
145
|
+
output: OutputType;
|
|
146
|
+
input: InputType;
|
|
147
|
+
scopes: string[];
|
|
148
|
+
config?: SlateAuthCallbackConfig;
|
|
149
|
+
}) => Promise<{
|
|
134
150
|
profile: Record<string, any>;
|
|
135
151
|
}>;
|
|
136
152
|
};
|
|
@@ -145,7 +161,10 @@ export type SlateAuthWithCustomData<InputType extends {}, OutputType extends {}>
|
|
|
145
161
|
|
|
146
162
|
docs?: SlateAuthDocsReference[];
|
|
147
163
|
|
|
148
|
-
getOutput: (ctx: { input: InputType }) => Promise<{
|
|
164
|
+
getOutput: (ctx: { input: InputType; config?: SlateAuthCallbackConfig }) => Promise<{
|
|
165
|
+
output: OutputType;
|
|
166
|
+
scopes?: string[];
|
|
167
|
+
}>;
|
|
149
168
|
|
|
150
169
|
handleTokenRefresh?: (ctx: {
|
|
151
170
|
output: OutputType;
|
|
@@ -153,6 +172,7 @@ export type SlateAuthWithCustomData<InputType extends {}, OutputType extends {}>
|
|
|
153
172
|
clientId: string;
|
|
154
173
|
clientSecret: string;
|
|
155
174
|
scopes: string[];
|
|
175
|
+
config?: SlateAuthCallbackConfig;
|
|
156
176
|
}) => Promise<{
|
|
157
177
|
output: OutputType;
|
|
158
178
|
input?: InputType;
|
|
@@ -165,7 +185,12 @@ export type SlateAuthWithCustomData<InputType extends {}, OutputType extends {}>
|
|
|
165
185
|
|
|
166
186
|
getDefaultInput?: () => Promise<InputType>;
|
|
167
187
|
|
|
168
|
-
getProfile?: (ctx: {
|
|
188
|
+
getProfile?: (ctx: {
|
|
189
|
+
output: OutputType;
|
|
190
|
+
input: InputType;
|
|
191
|
+
scopes: string[];
|
|
192
|
+
config?: SlateAuthCallbackConfig;
|
|
193
|
+
}) => Promise<{
|
|
169
194
|
profile: Record<string, any>;
|
|
170
195
|
}>;
|
|
171
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/context/context.ts
CHANGED
|
@@ -48,6 +48,14 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
|
|
|
48
48
|
: never;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
get registrationDetails() {
|
|
52
|
+
return Object.freeze(
|
|
53
|
+
(this.#input as any).registrationDetails
|
|
54
|
+
) as 'registrationDetails' extends keyof InputType
|
|
55
|
+
? InputType['registrationDetails']
|
|
56
|
+
: never;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
get auth() {
|
|
52
60
|
return Object.freeze(this.#auth);
|
|
53
61
|
}
|
|
@@ -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';
|