@slates/provider 1.0.0-rc.30 → 1.0.0-rc.32
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 +4 -0
- package/dist/index.d.cts +39 -25
- package/dist/index.d.ts +39 -25
- package/dist/index.module.js +4 -0
- package/package.json +1 -1
- package/src/auth/types.ts +55 -25
- package/src/error/base.test.ts +77 -0
- package/src/error/service.ts +3 -0
- package/src/error/types.ts +2 -0
- package/src/error/utils.ts +9 -0
package/dist/index.cjs
CHANGED
|
@@ -1309,6 +1309,8 @@ var sanitizeForBaggage = (value, options = {}) => {
|
|
|
1309
1309
|
return BAGGAGE_UNSERIALIZABLE_VALUE;
|
|
1310
1310
|
}
|
|
1311
1311
|
};
|
|
1312
|
+
var UPSTREAM_CODE_PATTERN = /^[\w.:-]{1,128}$/;
|
|
1313
|
+
var normalizeUpstreamCode = (value) => typeof value === "string" && UPSTREAM_CODE_PATTERN.test(value) ? value : void 0;
|
|
1312
1314
|
var normalizeSlateErrorInput = (input) => {
|
|
1313
1315
|
let code = input.code || DEFAULT_CODE;
|
|
1314
1316
|
return {
|
|
@@ -1323,6 +1325,7 @@ var normalizeSlateErrorInput = (input) => {
|
|
|
1323
1325
|
...input.upstream,
|
|
1324
1326
|
status: input.upstream?.status ?? input.status
|
|
1325
1327
|
}),
|
|
1328
|
+
upstreamCode: normalizeUpstreamCode(input.upstreamCode) ?? normalizeUpstreamCode(input.upstream?.code),
|
|
1326
1329
|
baggage: cleanRecord(input.baggage),
|
|
1327
1330
|
requestTraces: input.requestTraces
|
|
1328
1331
|
};
|
|
@@ -1785,6 +1788,7 @@ var mapServiceErrorToSlateErrorInput = (error, defaults = {}) => {
|
|
|
1785
1788
|
issues: defaults.issues ?? normalizeServiceErrorIssues(data.errors),
|
|
1786
1789
|
provider: defaults.provider,
|
|
1787
1790
|
upstream: defaults.upstream,
|
|
1791
|
+
upstreamCode: normalizeUpstreamCode(defaults.upstreamCode) ?? normalizeUpstreamCode(data.upstreamCode),
|
|
1788
1792
|
baggage: cleanRecord({
|
|
1789
1793
|
...defaults.baggage ?? {},
|
|
1790
1794
|
...getServiceErrorBaggage(data)
|
package/dist/index.d.cts
CHANGED
|
@@ -147,6 +147,35 @@ type SlateActionDocsReference = SlateTypedDocsReference<'docs.action.general'>;
|
|
|
147
147
|
type SlateAuthDocsReference = SlateTypedDocsReference<'docs.auth.oauth' | 'docs.auth.oauth_scopes' | 'docs.auth.token' | 'docs.auth.service_account' | 'docs.auth.custom'>;
|
|
148
148
|
|
|
149
149
|
type SlateAuthCallbackConfig = Record<string, any>;
|
|
150
|
+
type SlateOauthAuthorizationUrlContext<InputType> = {
|
|
151
|
+
redirectUri: string;
|
|
152
|
+
state: string;
|
|
153
|
+
input: InputType;
|
|
154
|
+
clientId: string;
|
|
155
|
+
clientSecret: string;
|
|
156
|
+
scopes: string[];
|
|
157
|
+
config?: SlateAuthCallbackConfig;
|
|
158
|
+
};
|
|
159
|
+
type SlateOauthAuthorizationUrlResult<InputType> = {
|
|
160
|
+
url: string;
|
|
161
|
+
input?: InputType;
|
|
162
|
+
callbackState?: Record<string, any>;
|
|
163
|
+
};
|
|
164
|
+
type SlateOauthCallbackContext<InputType> = {
|
|
165
|
+
code: string;
|
|
166
|
+
state: string;
|
|
167
|
+
redirectUri: string;
|
|
168
|
+
input: InputType;
|
|
169
|
+
clientId: string;
|
|
170
|
+
clientSecret: string;
|
|
171
|
+
scopes: string[];
|
|
172
|
+
callbackParams?: Record<string, string>;
|
|
173
|
+
callbackState: Record<string, any>;
|
|
174
|
+
config?: SlateAuthCallbackConfig;
|
|
175
|
+
};
|
|
176
|
+
type SlateOauthAdminCallbackContext<InputType> = Omit<SlateOauthCallbackContext<InputType>, 'code'> & {
|
|
177
|
+
code?: string;
|
|
178
|
+
};
|
|
150
179
|
type SlateAuthWithOauth<InputType extends {}, OutputType extends {
|
|
151
180
|
token: string;
|
|
152
181
|
refreshToken?: string;
|
|
@@ -162,6 +191,13 @@ type SlateAuthWithOauth<InputType extends {}, OutputType extends {
|
|
|
162
191
|
}[];
|
|
163
192
|
adapters?: string[];
|
|
164
193
|
syncTokensAcrossConnections?: boolean;
|
|
194
|
+
adminSetup?: {
|
|
195
|
+
required?: boolean;
|
|
196
|
+
getAuthorizationUrl?: (ctx: SlateOauthAuthorizationUrlContext<InputType>) => Promise<SlateOauthAuthorizationUrlResult<InputType>>;
|
|
197
|
+
handleCallback?: (ctx: SlateOauthAdminCallbackContext<InputType>) => Promise<{
|
|
198
|
+
instructions?: string;
|
|
199
|
+
}>;
|
|
200
|
+
};
|
|
165
201
|
docs?: SlateAuthDocsReference[];
|
|
166
202
|
inputSchema?: z.ZodType<InputType>;
|
|
167
203
|
onInputChanged?: (params: {
|
|
@@ -171,31 +207,8 @@ type SlateAuthWithOauth<InputType extends {}, OutputType extends {
|
|
|
171
207
|
input?: InputType;
|
|
172
208
|
} | undefined>;
|
|
173
209
|
getDefaultInput?: () => Promise<InputType>;
|
|
174
|
-
getAuthorizationUrl: (ctx:
|
|
175
|
-
|
|
176
|
-
state: string;
|
|
177
|
-
input: InputType;
|
|
178
|
-
clientId: string;
|
|
179
|
-
clientSecret: string;
|
|
180
|
-
scopes: string[];
|
|
181
|
-
config?: SlateAuthCallbackConfig;
|
|
182
|
-
}) => Promise<{
|
|
183
|
-
url: string;
|
|
184
|
-
input?: InputType;
|
|
185
|
-
callbackState?: Record<string, any>;
|
|
186
|
-
}>;
|
|
187
|
-
handleCallback: (ctx: {
|
|
188
|
-
code: string;
|
|
189
|
-
state: string;
|
|
190
|
-
redirectUri: string;
|
|
191
|
-
input: InputType;
|
|
192
|
-
clientId: string;
|
|
193
|
-
clientSecret: string;
|
|
194
|
-
scopes: string[];
|
|
195
|
-
callbackParams?: Record<string, string>;
|
|
196
|
-
callbackState: Record<string, any>;
|
|
197
|
-
config?: SlateAuthCallbackConfig;
|
|
198
|
-
}) => Promise<{
|
|
210
|
+
getAuthorizationUrl: (ctx: SlateOauthAuthorizationUrlContext<InputType>) => Promise<SlateOauthAuthorizationUrlResult<InputType>>;
|
|
211
|
+
handleCallback: (ctx: SlateOauthCallbackContext<InputType>) => Promise<{
|
|
199
212
|
output: OutputType;
|
|
200
213
|
input?: InputType;
|
|
201
214
|
scopes?: string[];
|
|
@@ -1064,6 +1077,7 @@ interface SlateErrorResponse {
|
|
|
1064
1077
|
issues?: SlateErrorIssue[];
|
|
1065
1078
|
provider?: SlateErrorProviderInfo;
|
|
1066
1079
|
upstream?: SlateErrorUpstreamInfo;
|
|
1080
|
+
upstreamCode?: string;
|
|
1067
1081
|
baggage?: Record<string, unknown>;
|
|
1068
1082
|
requestTraces?: SlateHttpTrace[];
|
|
1069
1083
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -147,6 +147,35 @@ type SlateActionDocsReference = SlateTypedDocsReference<'docs.action.general'>;
|
|
|
147
147
|
type SlateAuthDocsReference = SlateTypedDocsReference<'docs.auth.oauth' | 'docs.auth.oauth_scopes' | 'docs.auth.token' | 'docs.auth.service_account' | 'docs.auth.custom'>;
|
|
148
148
|
|
|
149
149
|
type SlateAuthCallbackConfig = Record<string, any>;
|
|
150
|
+
type SlateOauthAuthorizationUrlContext<InputType> = {
|
|
151
|
+
redirectUri: string;
|
|
152
|
+
state: string;
|
|
153
|
+
input: InputType;
|
|
154
|
+
clientId: string;
|
|
155
|
+
clientSecret: string;
|
|
156
|
+
scopes: string[];
|
|
157
|
+
config?: SlateAuthCallbackConfig;
|
|
158
|
+
};
|
|
159
|
+
type SlateOauthAuthorizationUrlResult<InputType> = {
|
|
160
|
+
url: string;
|
|
161
|
+
input?: InputType;
|
|
162
|
+
callbackState?: Record<string, any>;
|
|
163
|
+
};
|
|
164
|
+
type SlateOauthCallbackContext<InputType> = {
|
|
165
|
+
code: string;
|
|
166
|
+
state: string;
|
|
167
|
+
redirectUri: string;
|
|
168
|
+
input: InputType;
|
|
169
|
+
clientId: string;
|
|
170
|
+
clientSecret: string;
|
|
171
|
+
scopes: string[];
|
|
172
|
+
callbackParams?: Record<string, string>;
|
|
173
|
+
callbackState: Record<string, any>;
|
|
174
|
+
config?: SlateAuthCallbackConfig;
|
|
175
|
+
};
|
|
176
|
+
type SlateOauthAdminCallbackContext<InputType> = Omit<SlateOauthCallbackContext<InputType>, 'code'> & {
|
|
177
|
+
code?: string;
|
|
178
|
+
};
|
|
150
179
|
type SlateAuthWithOauth<InputType extends {}, OutputType extends {
|
|
151
180
|
token: string;
|
|
152
181
|
refreshToken?: string;
|
|
@@ -162,6 +191,13 @@ type SlateAuthWithOauth<InputType extends {}, OutputType extends {
|
|
|
162
191
|
}[];
|
|
163
192
|
adapters?: string[];
|
|
164
193
|
syncTokensAcrossConnections?: boolean;
|
|
194
|
+
adminSetup?: {
|
|
195
|
+
required?: boolean;
|
|
196
|
+
getAuthorizationUrl?: (ctx: SlateOauthAuthorizationUrlContext<InputType>) => Promise<SlateOauthAuthorizationUrlResult<InputType>>;
|
|
197
|
+
handleCallback?: (ctx: SlateOauthAdminCallbackContext<InputType>) => Promise<{
|
|
198
|
+
instructions?: string;
|
|
199
|
+
}>;
|
|
200
|
+
};
|
|
165
201
|
docs?: SlateAuthDocsReference[];
|
|
166
202
|
inputSchema?: z.ZodType<InputType>;
|
|
167
203
|
onInputChanged?: (params: {
|
|
@@ -171,31 +207,8 @@ type SlateAuthWithOauth<InputType extends {}, OutputType extends {
|
|
|
171
207
|
input?: InputType;
|
|
172
208
|
} | undefined>;
|
|
173
209
|
getDefaultInput?: () => Promise<InputType>;
|
|
174
|
-
getAuthorizationUrl: (ctx:
|
|
175
|
-
|
|
176
|
-
state: string;
|
|
177
|
-
input: InputType;
|
|
178
|
-
clientId: string;
|
|
179
|
-
clientSecret: string;
|
|
180
|
-
scopes: string[];
|
|
181
|
-
config?: SlateAuthCallbackConfig;
|
|
182
|
-
}) => Promise<{
|
|
183
|
-
url: string;
|
|
184
|
-
input?: InputType;
|
|
185
|
-
callbackState?: Record<string, any>;
|
|
186
|
-
}>;
|
|
187
|
-
handleCallback: (ctx: {
|
|
188
|
-
code: string;
|
|
189
|
-
state: string;
|
|
190
|
-
redirectUri: string;
|
|
191
|
-
input: InputType;
|
|
192
|
-
clientId: string;
|
|
193
|
-
clientSecret: string;
|
|
194
|
-
scopes: string[];
|
|
195
|
-
callbackParams?: Record<string, string>;
|
|
196
|
-
callbackState: Record<string, any>;
|
|
197
|
-
config?: SlateAuthCallbackConfig;
|
|
198
|
-
}) => Promise<{
|
|
210
|
+
getAuthorizationUrl: (ctx: SlateOauthAuthorizationUrlContext<InputType>) => Promise<SlateOauthAuthorizationUrlResult<InputType>>;
|
|
211
|
+
handleCallback: (ctx: SlateOauthCallbackContext<InputType>) => Promise<{
|
|
199
212
|
output: OutputType;
|
|
200
213
|
input?: InputType;
|
|
201
214
|
scopes?: string[];
|
|
@@ -1064,6 +1077,7 @@ interface SlateErrorResponse {
|
|
|
1064
1077
|
issues?: SlateErrorIssue[];
|
|
1065
1078
|
provider?: SlateErrorProviderInfo;
|
|
1066
1079
|
upstream?: SlateErrorUpstreamInfo;
|
|
1080
|
+
upstreamCode?: string;
|
|
1067
1081
|
baggage?: Record<string, unknown>;
|
|
1068
1082
|
requestTraces?: SlateHttpTrace[];
|
|
1069
1083
|
}
|
package/dist/index.module.js
CHANGED
|
@@ -1184,6 +1184,8 @@ var sanitizeForBaggage = (value, options = {}) => {
|
|
|
1184
1184
|
return BAGGAGE_UNSERIALIZABLE_VALUE;
|
|
1185
1185
|
}
|
|
1186
1186
|
};
|
|
1187
|
+
var UPSTREAM_CODE_PATTERN = /^[\w.:-]{1,128}$/;
|
|
1188
|
+
var normalizeUpstreamCode = (value) => typeof value === "string" && UPSTREAM_CODE_PATTERN.test(value) ? value : void 0;
|
|
1187
1189
|
var normalizeSlateErrorInput = (input) => {
|
|
1188
1190
|
let code = input.code || DEFAULT_CODE;
|
|
1189
1191
|
return {
|
|
@@ -1198,6 +1200,7 @@ var normalizeSlateErrorInput = (input) => {
|
|
|
1198
1200
|
...input.upstream,
|
|
1199
1201
|
status: input.upstream?.status ?? input.status
|
|
1200
1202
|
}),
|
|
1203
|
+
upstreamCode: normalizeUpstreamCode(input.upstreamCode) ?? normalizeUpstreamCode(input.upstream?.code),
|
|
1201
1204
|
baggage: cleanRecord(input.baggage),
|
|
1202
1205
|
requestTraces: input.requestTraces
|
|
1203
1206
|
};
|
|
@@ -1660,6 +1663,7 @@ var mapServiceErrorToSlateErrorInput = (error, defaults = {}) => {
|
|
|
1660
1663
|
issues: defaults.issues ?? normalizeServiceErrorIssues(data.errors),
|
|
1661
1664
|
provider: defaults.provider,
|
|
1662
1665
|
upstream: defaults.upstream,
|
|
1666
|
+
upstreamCode: normalizeUpstreamCode(defaults.upstreamCode) ?? normalizeUpstreamCode(data.upstreamCode),
|
|
1663
1667
|
baggage: cleanRecord({
|
|
1664
1668
|
...defaults.baggage ?? {},
|
|
1665
1669
|
...getServiceErrorBaggage(data)
|
package/package.json
CHANGED
package/src/auth/types.ts
CHANGED
|
@@ -3,6 +3,42 @@ import type { SlateAuthDocsReference } from '../docs';
|
|
|
3
3
|
|
|
4
4
|
type SlateAuthCallbackConfig = Record<string, any>;
|
|
5
5
|
|
|
6
|
+
type SlateOauthAuthorizationUrlContext<InputType> = {
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
state: string;
|
|
9
|
+
input: InputType;
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
scopes: string[];
|
|
13
|
+
config?: SlateAuthCallbackConfig;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type SlateOauthAuthorizationUrlResult<InputType> = {
|
|
17
|
+
url: string;
|
|
18
|
+
input?: InputType;
|
|
19
|
+
callbackState?: Record<string, any>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type SlateOauthCallbackContext<InputType> = {
|
|
23
|
+
code: string;
|
|
24
|
+
state: string;
|
|
25
|
+
redirectUri: string;
|
|
26
|
+
input: InputType;
|
|
27
|
+
clientId: string;
|
|
28
|
+
clientSecret: string;
|
|
29
|
+
scopes: string[];
|
|
30
|
+
callbackParams?: Record<string, string>;
|
|
31
|
+
callbackState: Record<string, any>;
|
|
32
|
+
config?: SlateAuthCallbackConfig;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type SlateOauthAdminCallbackContext<InputType> = Omit<
|
|
36
|
+
SlateOauthCallbackContext<InputType>,
|
|
37
|
+
'code'
|
|
38
|
+
> & {
|
|
39
|
+
code?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
6
42
|
export type SlateAuthWithOauth<
|
|
7
43
|
InputType extends {},
|
|
8
44
|
OutputType extends {
|
|
@@ -28,6 +64,21 @@ export type SlateAuthWithOauth<
|
|
|
28
64
|
// connection of one installation, so a refresh must update all of them.
|
|
29
65
|
syncTokensAcrossConnections?: boolean;
|
|
30
66
|
|
|
67
|
+
// Missing hooks fall back to the regular flow (callback output discarded).
|
|
68
|
+
// Returned instructions are markdown shown to the admin. `required` means users
|
|
69
|
+
// can't connect until an admin has completed setup; otherwise it is optional.
|
|
70
|
+
adminSetup?: {
|
|
71
|
+
required?: boolean;
|
|
72
|
+
|
|
73
|
+
getAuthorizationUrl?: (
|
|
74
|
+
ctx: SlateOauthAuthorizationUrlContext<InputType>
|
|
75
|
+
) => Promise<SlateOauthAuthorizationUrlResult<InputType>>;
|
|
76
|
+
|
|
77
|
+
handleCallback?: (
|
|
78
|
+
ctx: SlateOauthAdminCallbackContext<InputType>
|
|
79
|
+
) => Promise<{ instructions?: string }>;
|
|
80
|
+
};
|
|
81
|
+
|
|
31
82
|
docs?: SlateAuthDocsReference[];
|
|
32
83
|
|
|
33
84
|
inputSchema?: z.ZodType<InputType>;
|
|
@@ -39,32 +90,11 @@ export type SlateAuthWithOauth<
|
|
|
39
90
|
|
|
40
91
|
getDefaultInput?: () => Promise<InputType>;
|
|
41
92
|
|
|
42
|
-
getAuthorizationUrl: (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
input: InputType;
|
|
46
|
-
clientId: string;
|
|
47
|
-
clientSecret: string;
|
|
48
|
-
scopes: string[];
|
|
49
|
-
config?: SlateAuthCallbackConfig;
|
|
50
|
-
}) => Promise<{
|
|
51
|
-
url: string;
|
|
52
|
-
input?: InputType;
|
|
53
|
-
callbackState?: Record<string, any>;
|
|
54
|
-
}>;
|
|
93
|
+
getAuthorizationUrl: (
|
|
94
|
+
ctx: SlateOauthAuthorizationUrlContext<InputType>
|
|
95
|
+
) => Promise<SlateOauthAuthorizationUrlResult<InputType>>;
|
|
55
96
|
|
|
56
|
-
handleCallback: (ctx: {
|
|
57
|
-
code: string;
|
|
58
|
-
state: string;
|
|
59
|
-
redirectUri: string;
|
|
60
|
-
input: InputType;
|
|
61
|
-
clientId: string;
|
|
62
|
-
clientSecret: string;
|
|
63
|
-
scopes: string[];
|
|
64
|
-
callbackParams?: Record<string, string>;
|
|
65
|
-
callbackState: Record<string, any>;
|
|
66
|
-
config?: SlateAuthCallbackConfig;
|
|
67
|
-
}) => Promise<{
|
|
97
|
+
handleCallback: (ctx: SlateOauthCallbackContext<InputType>) => Promise<{
|
|
68
98
|
output: OutputType;
|
|
69
99
|
input?: InputType;
|
|
70
100
|
scopes?: string[];
|
package/src/error/base.test.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '@lowerdeck/error';
|
|
18
18
|
import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios';
|
|
19
19
|
import { describe, expect, it } from 'vitest';
|
|
20
|
+
import { createApiServiceError } from './api';
|
|
20
21
|
import { SlateError } from './base';
|
|
21
22
|
|
|
22
23
|
let createConfig = (overrides: Partial<InternalAxiosRequestConfig> = {}) =>
|
|
@@ -175,4 +176,80 @@ describe('SlateError', () => {
|
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
});
|
|
179
|
+
|
|
180
|
+
it('exposes the upstream API error code as a top-level upstreamCode', () => {
|
|
181
|
+
expect(
|
|
182
|
+
new SlateError({ code: 'upstream.error', message: 'x', upstream: { code: 'E42' } }).data
|
|
183
|
+
.upstreamCode
|
|
184
|
+
).toBe('E42');
|
|
185
|
+
expect(
|
|
186
|
+
new SlateError({
|
|
187
|
+
code: 'upstream.error',
|
|
188
|
+
message: 'x',
|
|
189
|
+
upstream: { code: 'nested' },
|
|
190
|
+
upstreamCode: 'explicit'
|
|
191
|
+
}).data.upstreamCode
|
|
192
|
+
).toBe('explicit');
|
|
193
|
+
expect(
|
|
194
|
+
SlateError.fromServiceError(
|
|
195
|
+
createApiServiceError('x', { upstreamCode: 'card_declined' })
|
|
196
|
+
).data.upstreamCode
|
|
197
|
+
).toBe('card_declined');
|
|
198
|
+
expect(new SlateError({ code: 'upstream.error', message: 'x' }).data.upstreamCode).toBe(
|
|
199
|
+
undefined
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('accepts only code-shaped upstreamCode values', () => {
|
|
204
|
+
for (let code of [
|
|
205
|
+
'AADSTS700016',
|
|
206
|
+
'invalid_grant',
|
|
207
|
+
'RATE_LIMITED',
|
|
208
|
+
'request.bad',
|
|
209
|
+
'a:b-c'
|
|
210
|
+
]) {
|
|
211
|
+
expect(
|
|
212
|
+
new SlateError({ code: 'upstream.error', message: 'x', upstreamCode: code }).data
|
|
213
|
+
.upstreamCode
|
|
214
|
+
).toBe(code);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (let code of ['', 'x'.repeat(129), 'has space', 'token=abc&x', 'line\nbreak', '`x`']) {
|
|
218
|
+
expect(
|
|
219
|
+
new SlateError({ code: 'upstream.error', message: 'x', upstreamCode: code }).data
|
|
220
|
+
.upstreamCode
|
|
221
|
+
).toBe(undefined);
|
|
222
|
+
expect(
|
|
223
|
+
SlateError.fromServiceError(createApiServiceError('x', { upstreamCode: code })).data
|
|
224
|
+
.upstreamCode
|
|
225
|
+
).toBe(undefined);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
expect(
|
|
229
|
+
SlateError.fromServiceError(
|
|
230
|
+
createApiServiceError('x', { upstreamCode: { nested: true } as unknown as string })
|
|
231
|
+
).data.upstreamCode
|
|
232
|
+
).toBe(undefined);
|
|
233
|
+
expect(
|
|
234
|
+
new SlateError({ code: 'upstream.error', message: 'x', upstreamCode: 'x'.repeat(128) })
|
|
235
|
+
.data.upstreamCode
|
|
236
|
+
).toBe('x'.repeat(128));
|
|
237
|
+
expect(
|
|
238
|
+
new SlateError({
|
|
239
|
+
code: 'upstream.error',
|
|
240
|
+
message: 'x',
|
|
241
|
+
upstreamCode: 'bad value',
|
|
242
|
+
upstream: { code: 'fallback_code' }
|
|
243
|
+
}).data.upstreamCode
|
|
244
|
+
).toBe('fallback_code');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('also keeps upstreamCode in service error baggage for older hubs', () => {
|
|
248
|
+
let error = SlateError.fromServiceError(
|
|
249
|
+
createApiServiceError('x', { upstreamCode: 'card_declined' })
|
|
250
|
+
);
|
|
251
|
+
expect(error.data.baggage?.serviceErrorData).toMatchObject({
|
|
252
|
+
upstreamCode: 'card_declined'
|
|
253
|
+
});
|
|
254
|
+
});
|
|
178
255
|
});
|
package/src/error/service.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isRecord,
|
|
8
8
|
normalizeKind,
|
|
9
9
|
normalizeRetryable,
|
|
10
|
+
normalizeUpstreamCode,
|
|
10
11
|
sanitizeForBaggage
|
|
11
12
|
} from './utils';
|
|
12
13
|
|
|
@@ -162,6 +163,8 @@ export let mapServiceErrorToSlateErrorInput = (
|
|
|
162
163
|
issues: defaults.issues ?? normalizeServiceErrorIssues(data.errors),
|
|
163
164
|
provider: defaults.provider,
|
|
164
165
|
upstream: defaults.upstream,
|
|
166
|
+
upstreamCode:
|
|
167
|
+
normalizeUpstreamCode(defaults.upstreamCode) ?? normalizeUpstreamCode(data.upstreamCode),
|
|
165
168
|
baggage: cleanRecord({
|
|
166
169
|
...(defaults.baggage ?? {}),
|
|
167
170
|
...getServiceErrorBaggage(data)
|
package/src/error/types.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface SlateErrorResponse {
|
|
|
45
45
|
issues?: SlateErrorIssue[];
|
|
46
46
|
provider?: SlateErrorProviderInfo;
|
|
47
47
|
upstream?: SlateErrorUpstreamInfo;
|
|
48
|
+
// The upstream API's own error code, next to the normalized protocol `code`.
|
|
49
|
+
upstreamCode?: string;
|
|
48
50
|
baggage?: Record<string, unknown>;
|
|
49
51
|
requestTraces?: SlateHttpTrace[];
|
|
50
52
|
}
|
package/src/error/utils.ts
CHANGED
|
@@ -389,6 +389,13 @@ export let sanitizeForBaggage = (
|
|
|
389
389
|
}
|
|
390
390
|
};
|
|
391
391
|
|
|
392
|
+
let UPSTREAM_CODE_PATTERN = /^[\w.:-]{1,128}$/;
|
|
393
|
+
|
|
394
|
+
// upstreamCode is copied from untrusted upstream responses to the top level of the
|
|
395
|
+
// error, outside the baggage sanitizer, so anything not code-shaped is dropped.
|
|
396
|
+
export let normalizeUpstreamCode = (value: unknown): string | undefined =>
|
|
397
|
+
typeof value === 'string' && UPSTREAM_CODE_PATTERN.test(value) ? value : undefined;
|
|
398
|
+
|
|
392
399
|
export let normalizeSlateErrorInput = (input: SlateErrorInput): SlateErrorResponse => {
|
|
393
400
|
let code = input.code || DEFAULT_CODE;
|
|
394
401
|
return {
|
|
@@ -405,6 +412,8 @@ export let normalizeSlateErrorInput = (input: SlateErrorInput): SlateErrorRespon
|
|
|
405
412
|
...input.upstream,
|
|
406
413
|
status: input.upstream?.status ?? input.status
|
|
407
414
|
}) as SlateErrorUpstreamInfo | undefined,
|
|
415
|
+
upstreamCode:
|
|
416
|
+
normalizeUpstreamCode(input.upstreamCode) ?? normalizeUpstreamCode(input.upstream?.code),
|
|
408
417
|
baggage: cleanRecord(input.baggage),
|
|
409
418
|
requestTraces: input.requestTraces
|
|
410
419
|
};
|