@windstream/react-shared-components 0.2.60 → 0.2.61
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/contentful/index.d.ts +27 -1
- package/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +4 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/brand-button/index.tsx +1 -1
- package/src/contentful/blocks/cards/product-card/index.test.tsx +14 -1
- package/src/contentful/blocks/cards/product-card/index.tsx +189 -136
- package/src/contentful/blocks/cards/product-card/types.ts +1 -1
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +59 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +23 -0
- package/src/contentful/blocks/carousel/helper.tsx +18 -5
- package/src/contentful/blocks/carousel/types.ts +3 -0
- package/src/contentful/blocks/dynamic-form/conditional-fields.test.tsx +3 -0
- package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +30 -4
- package/src/contentful/blocks/dynamic-form/context.ts +2 -0
- package/src/contentful/blocks/dynamic-form/field-map.test.tsx +21 -0
- package/src/contentful/blocks/dynamic-form/field-map.tsx +15 -0
- package/src/contentful/blocks/dynamic-form/form-field.tsx +2 -0
- package/src/contentful/blocks/dynamic-form/index.tsx +48 -16
- package/src/contentful/blocks/dynamic-form/render.tsx +1 -0
- package/src/contentful/blocks/dynamic-form/types.ts +26 -0
- package/src/contentful/blocks/dynamic-form/utils.test.ts +86 -0
- package/src/contentful/blocks/dynamic-form/utils.ts +57 -0
|
@@ -189,6 +189,27 @@ describe("field-map", () => {
|
|
|
189
189
|
fireEvent.click(screen.getAllByRole("checkbox")[0]);
|
|
190
190
|
expect(handleInputChange).toHaveBeenCalledWith("b");
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
it("gives each option a unique id so labels target the correct input", () => {
|
|
194
|
+
render(
|
|
195
|
+
<Checkbox {...baseProps({ dataSource, formData: {} })} />
|
|
196
|
+
);
|
|
197
|
+
const ids = screen
|
|
198
|
+
.getAllByRole("checkbox")
|
|
199
|
+
.map(input => input.getAttribute("id"));
|
|
200
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("selects a non-first option when its label is clicked", () => {
|
|
204
|
+
const handleInputChange = jest.fn();
|
|
205
|
+
render(
|
|
206
|
+
<Checkbox
|
|
207
|
+
{...baseProps({ dataSource, formData: {}, handleInputChange })}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
fireEvent.click(screen.getByText("B"));
|
|
211
|
+
expect(handleInputChange).toHaveBeenCalledWith("b");
|
|
212
|
+
});
|
|
192
213
|
});
|
|
193
214
|
|
|
194
215
|
describe("DatePickerField", () => {
|
|
@@ -45,8 +45,22 @@ const TextInputField: React.FC<FieldRenderProps> = args => {
|
|
|
45
45
|
handleInputChange,
|
|
46
46
|
fieldId,
|
|
47
47
|
errorClass,
|
|
48
|
+
enableAddressAutocomplete,
|
|
48
49
|
} = args;
|
|
49
50
|
|
|
51
|
+
const context = useContext(FormContext);
|
|
52
|
+
const AddressAutocomplete = context?.renderAddressAutocomplete;
|
|
53
|
+
const shouldUseAddressAutocomplete =
|
|
54
|
+
!!enableAddressAutocomplete &&
|
|
55
|
+
!!AddressAutocomplete &&
|
|
56
|
+
textInputType !== "phone" &&
|
|
57
|
+
textInputType !== "email" &&
|
|
58
|
+
textInputType !== "multiline";
|
|
59
|
+
|
|
60
|
+
if (shouldUseAddressAutocomplete && AddressAutocomplete) {
|
|
61
|
+
return <AddressAutocomplete {...args} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
if (textInputType === "phone") {
|
|
51
65
|
return (
|
|
52
66
|
<div className="w-full">
|
|
@@ -160,6 +174,7 @@ const CheckboxInputField: React.FC<FieldRenderProps> = args => {
|
|
|
160
174
|
<div key={`${fieldId}-${idx}`} className="w-full lg:w-[384px]">
|
|
161
175
|
<Checkbox
|
|
162
176
|
name={fieldId}
|
|
177
|
+
id={`${fieldId}-${idx}`}
|
|
163
178
|
label={data.label}
|
|
164
179
|
value={data.value}
|
|
165
180
|
checked={selectedValues.includes(data.value)}
|
|
@@ -55,6 +55,7 @@ export const FormFieldElement: React.FC<FormFieldElementProps> = props => {
|
|
|
55
55
|
validationType,
|
|
56
56
|
applyRegularExpression,
|
|
57
57
|
regexPattern,
|
|
58
|
+
enableAddressAutocomplete,
|
|
58
59
|
} = props;
|
|
59
60
|
|
|
60
61
|
const [error, setError] = useState<FieldError>({
|
|
@@ -203,6 +204,7 @@ export const FormFieldElement: React.FC<FormFieldElementProps> = props => {
|
|
|
203
204
|
hasError={hasError}
|
|
204
205
|
existingDataSource={existingDataSource}
|
|
205
206
|
isConsentCheckbox={isConsentCheckbox}
|
|
207
|
+
enableAddressAutocomplete={enableAddressAutocomplete}
|
|
206
208
|
data-testid="form-component"
|
|
207
209
|
/>
|
|
208
210
|
</div>
|
|
@@ -21,10 +21,12 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
arrayForFieldValidations,
|
|
23
23
|
buildSftpMap,
|
|
24
|
+
extractErrorDetails,
|
|
24
25
|
filterActionsForEmailTemplate,
|
|
25
26
|
formPayloadCreation,
|
|
26
27
|
generateSubmissionId,
|
|
27
28
|
getInvalidFields,
|
|
29
|
+
sanitizeError,
|
|
28
30
|
shouldShowPopup,
|
|
29
31
|
} from "./utils";
|
|
30
32
|
|
|
@@ -117,6 +119,8 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
117
119
|
onError,
|
|
118
120
|
fieldComponentOverrides,
|
|
119
121
|
resolveExistingDataSource,
|
|
122
|
+
renderAddressAutocomplete,
|
|
123
|
+
renderSubmitButton,
|
|
120
124
|
} = props;
|
|
121
125
|
|
|
122
126
|
const submissionIdRef = useRef<string>(generateSubmissionId());
|
|
@@ -182,6 +186,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
182
186
|
setFiles: updateFiles,
|
|
183
187
|
resolveExistingDataSource,
|
|
184
188
|
fieldComponentOverrides,
|
|
189
|
+
renderAddressAutocomplete,
|
|
185
190
|
onError,
|
|
186
191
|
}),
|
|
187
192
|
[
|
|
@@ -194,6 +199,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
194
199
|
updateFiles,
|
|
195
200
|
resolveExistingDataSource,
|
|
196
201
|
fieldComponentOverrides,
|
|
202
|
+
renderAddressAutocomplete,
|
|
197
203
|
onError,
|
|
198
204
|
]
|
|
199
205
|
);
|
|
@@ -266,25 +272,38 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
266
272
|
});
|
|
267
273
|
} catch (error) {
|
|
268
274
|
errorActions.push(action);
|
|
275
|
+
const { errorMessage, errorStatus, errorCode } =
|
|
276
|
+
extractErrorDetails(error);
|
|
269
277
|
onTrack?.(`${segmentEventId}_${action}_failure`, {
|
|
270
278
|
...payload,
|
|
271
279
|
eventSuccess: false,
|
|
280
|
+
errorMessage,
|
|
281
|
+
errorStatus,
|
|
282
|
+
errorCode,
|
|
272
283
|
});
|
|
273
284
|
onError?.({
|
|
274
285
|
type: "action_failure",
|
|
275
|
-
error,
|
|
286
|
+
error: sanitizeError(error),
|
|
276
287
|
metadata: { action, submissionId: submissionIdRef.current },
|
|
277
288
|
});
|
|
278
289
|
}
|
|
279
290
|
} else {
|
|
280
291
|
errorActions.push(action);
|
|
292
|
+
const unsupportedError = new Error(
|
|
293
|
+
`Action "${action}" is not supported`
|
|
294
|
+
);
|
|
295
|
+
const { errorMessage, errorStatus, errorCode } =
|
|
296
|
+
extractErrorDetails(unsupportedError);
|
|
281
297
|
onTrack?.(`${segmentEventId}_${action}_failure`, {
|
|
282
298
|
...payload,
|
|
283
299
|
eventSuccess: false,
|
|
300
|
+
errorMessage,
|
|
301
|
+
errorStatus,
|
|
302
|
+
errorCode,
|
|
284
303
|
});
|
|
285
304
|
onError?.({
|
|
286
305
|
type: "unsupported_action",
|
|
287
|
-
error:
|
|
306
|
+
error: unsupportedError,
|
|
288
307
|
metadata: { action },
|
|
289
308
|
});
|
|
290
309
|
}
|
|
@@ -302,7 +321,11 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
302
321
|
const newFormData = applyPresetValidations();
|
|
303
322
|
|
|
304
323
|
try {
|
|
305
|
-
const extractedArray = arrayForFieldValidations(
|
|
324
|
+
const extractedArray = arrayForFieldValidations(
|
|
325
|
+
fields,
|
|
326
|
+
false,
|
|
327
|
+
newFormData
|
|
328
|
+
);
|
|
306
329
|
const sftpMap = buildSftpMap(fields, newFormData);
|
|
307
330
|
const invalidFields = getInvalidFields(extractedArray, newFormData);
|
|
308
331
|
setSubmitErrorFields(invalidFields);
|
|
@@ -433,7 +456,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
433
456
|
|
|
434
457
|
onError?.({
|
|
435
458
|
type: errorType,
|
|
436
|
-
error,
|
|
459
|
+
error: sanitizeError(error),
|
|
437
460
|
metadata: {
|
|
438
461
|
formName,
|
|
439
462
|
submissionId: submissionIdRef.current,
|
|
@@ -522,18 +545,27 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
522
545
|
</Text>
|
|
523
546
|
)}
|
|
524
547
|
<div className="flex justify-center">
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
548
|
+
{renderSubmitButton ? (
|
|
549
|
+
renderSubmitButton({
|
|
550
|
+
isLoading,
|
|
551
|
+
disabled: hasError || isLoading,
|
|
552
|
+
submitLabel,
|
|
553
|
+
onSubmit: handleSubmit,
|
|
554
|
+
})
|
|
555
|
+
) : (
|
|
556
|
+
<Button
|
|
557
|
+
type="submit"
|
|
558
|
+
disabled={hasError || isLoading}
|
|
559
|
+
aria-label="Submit Form"
|
|
560
|
+
className={cx(
|
|
561
|
+
"button-submit mt-3 inline-flex h-[64px] w-full items-center justify-center gap-2 whitespace-nowrap rounded-full bg-bg-fill-brand px-[60px] text-base font-semibold text-text-brand-on-bg-fill transition hover:bg-bg-fill-brand-hover disabled:bg-bg-fill-brand-disabled md:mt-8 md:w-auto",
|
|
562
|
+
ctaClassName
|
|
563
|
+
)}
|
|
564
|
+
>
|
|
565
|
+
{isLoading && <Spinner />}
|
|
566
|
+
{submitLabel}
|
|
567
|
+
</Button>
|
|
568
|
+
)}
|
|
537
569
|
</div>
|
|
538
570
|
</form>
|
|
539
571
|
</FormContext.Provider>
|
|
@@ -51,6 +51,8 @@ export interface DynamicFormFieldConfig {
|
|
|
51
51
|
regexPattern?: string;
|
|
52
52
|
applyPresetValidations?: boolean;
|
|
53
53
|
selectPresetValidationsToApply?: string;
|
|
54
|
+
/** When true, a Text field renders the app-injected address autocomplete. */
|
|
55
|
+
enableAddressAutocomplete?: boolean;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface ConditionalFieldConfig {
|
|
@@ -60,6 +62,11 @@ export interface ConditionalFieldConfig {
|
|
|
60
62
|
dataSource?: DynamicFormFieldOption[] | null;
|
|
61
63
|
note?: string;
|
|
62
64
|
formFieldsToShow: DynamicFormItem[];
|
|
65
|
+
/**
|
|
66
|
+
* When true, the app has opted this field into the grouped layout styling.
|
|
67
|
+
* Injected by the consuming app so the shared package stays route-agnostic.
|
|
68
|
+
*/
|
|
69
|
+
grouped?: boolean;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/**
|
|
@@ -128,6 +135,7 @@ export interface FieldRenderProps {
|
|
|
128
135
|
hasError: boolean;
|
|
129
136
|
existingDataSource?: string;
|
|
130
137
|
isConsentCheckbox?: boolean;
|
|
138
|
+
enableAddressAutocomplete?: boolean;
|
|
131
139
|
[key: string]: any;
|
|
132
140
|
}
|
|
133
141
|
|
|
@@ -208,4 +216,22 @@ export interface DynamicFormProps {
|
|
|
208
216
|
Record<DynamicFormFieldType, React.FC<FieldRenderProps>>
|
|
209
217
|
>;
|
|
210
218
|
resolveExistingDataSource?: ExistingDataSourceResolver;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* App-injected renderer for address-autocomplete Text fields. The package is
|
|
222
|
+
* transport-agnostic, so address suggestion fetching lives in the app.
|
|
223
|
+
*/
|
|
224
|
+
renderAddressAutocomplete?: React.FC<FieldRenderProps>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* App-injected submit button, so the app can attach its own CTA analytics,
|
|
228
|
+
* query-param preservation and animation. When omitted a default button is
|
|
229
|
+
* rendered.
|
|
230
|
+
*/
|
|
231
|
+
renderSubmitButton?: (args: {
|
|
232
|
+
isLoading: boolean;
|
|
233
|
+
disabled: boolean;
|
|
234
|
+
submitLabel: string;
|
|
235
|
+
onSubmit: (event: any) => void;
|
|
236
|
+
}) => React.ReactNode;
|
|
211
237
|
}
|
|
@@ -2,12 +2,14 @@ import { DynamicFormItem } from "./types";
|
|
|
2
2
|
import {
|
|
3
3
|
arrayForFieldValidations,
|
|
4
4
|
buildSftpMap,
|
|
5
|
+
extractErrorDetails,
|
|
5
6
|
filterActionsForEmailTemplate,
|
|
6
7
|
formPayloadCreation,
|
|
7
8
|
generateSubmissionId,
|
|
8
9
|
getInvalidFields,
|
|
9
10
|
groupFieldsForLayout,
|
|
10
11
|
processFields,
|
|
12
|
+
sanitizeErrorMessage,
|
|
11
13
|
shouldShowPopup,
|
|
12
14
|
} from "./utils";
|
|
13
15
|
|
|
@@ -235,4 +237,88 @@ describe("dynamic-form/utils", () => {
|
|
|
235
237
|
}
|
|
236
238
|
});
|
|
237
239
|
});
|
|
240
|
+
|
|
241
|
+
describe("sanitizeErrorMessage", () => {
|
|
242
|
+
it("redacts bearer tokens including +, /, and = characters", () => {
|
|
243
|
+
const credential = "ab+cd/ef=gh";
|
|
244
|
+
const result = sanitizeErrorMessage(
|
|
245
|
+
`Request failed with ${"Bearer"} ${credential} and retry`
|
|
246
|
+
);
|
|
247
|
+
expect(result).toBe("Request failed with Bearer [REDACTED] and retry");
|
|
248
|
+
expect(result).not.toContain(credential);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("redacts Authorization headers", () => {
|
|
252
|
+
const secret = "dXNlcjpwYXNzd29yZA==";
|
|
253
|
+
const result = sanitizeErrorMessage(`Authorization: ${secret}`);
|
|
254
|
+
expect(result).toBe("Authorization: [REDACTED]");
|
|
255
|
+
expect(result).not.toContain(secret);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("redacts api key, token, secret, and password values", () => {
|
|
259
|
+
expect(sanitizeErrorMessage("api_key=abc123&foo=bar")).toBe(
|
|
260
|
+
"api_key=[REDACTED]&foo=bar"
|
|
261
|
+
);
|
|
262
|
+
expect(sanitizeErrorMessage('password: "s3cr3t!"')).toBe(
|
|
263
|
+
"password: [REDACTED]"
|
|
264
|
+
);
|
|
265
|
+
expect(sanitizeErrorMessage("access-token: 'tok/en+val='")).toBe(
|
|
266
|
+
"access-token: [REDACTED]"
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("redacts a full quoted value through the closing quote", () => {
|
|
271
|
+
const result = sanitizeErrorMessage('secret="my secret with spaces"');
|
|
272
|
+
expect(result).toBe("secret=[REDACTED]");
|
|
273
|
+
expect(result).not.toContain("my secret with spaces");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("leaves messages without secrets untouched", () => {
|
|
277
|
+
expect(sanitizeErrorMessage("Network timeout")).toBe("Network timeout");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("extractErrorDetails", () => {
|
|
282
|
+
it("extracts a sanitized message from a native Error", () => {
|
|
283
|
+
const credential = "xyz+123/456=";
|
|
284
|
+
const { errorMessage } = extractErrorDetails(
|
|
285
|
+
new Error(`Failed with ${"Bearer"} ${credential}`)
|
|
286
|
+
);
|
|
287
|
+
expect(errorMessage).toBe("Failed with Bearer [REDACTED]");
|
|
288
|
+
expect(errorMessage).not.toContain(credential);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("stringifies non-Error values", () => {
|
|
292
|
+
expect(extractErrorDetails("boom")).toEqual({
|
|
293
|
+
errorMessage: "boom",
|
|
294
|
+
errorStatus: undefined,
|
|
295
|
+
errorCode: undefined,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("extracts status from an Axios-style response", () => {
|
|
300
|
+
const { errorStatus } = extractErrorDetails({
|
|
301
|
+
message: "Request failed",
|
|
302
|
+
response: { status: 401 },
|
|
303
|
+
});
|
|
304
|
+
expect(errorStatus).toBe(401);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("extracts status and code from a wrapped error cause", () => {
|
|
308
|
+
const error = Object.assign(new Error("Wrapped failure"), {
|
|
309
|
+
cause: { response: { status: 503 }, code: "ETIMEDOUT" },
|
|
310
|
+
});
|
|
311
|
+
const { errorStatus, errorCode } = extractErrorDetails(error);
|
|
312
|
+
expect(errorStatus).toBe(503);
|
|
313
|
+
expect(errorCode).toBe("ETIMEDOUT");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("omits a non-string code so objects cannot leak downstream", () => {
|
|
317
|
+
const { errorCode } = extractErrorDetails({
|
|
318
|
+
message: "bad",
|
|
319
|
+
code: { secret: "value" },
|
|
320
|
+
});
|
|
321
|
+
expect(errorCode).toBeUndefined();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
238
324
|
});
|
|
@@ -252,4 +252,61 @@ export const filterActionsForEmailTemplate = (
|
|
|
252
252
|
return actions;
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Redact common secret/PII patterns (bearer tokens, api keys, auth headers,
|
|
257
|
+
* token/key values) from an error message before it is forwarded to analytics,
|
|
258
|
+
* so credentials never leak into downstream tracking events.
|
|
259
|
+
*/
|
|
260
|
+
export const sanitizeErrorMessage = (message: string): string => {
|
|
261
|
+
return message
|
|
262
|
+
.replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]")
|
|
263
|
+
.replace(
|
|
264
|
+
/["']?Authorization["']?\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/gi,
|
|
265
|
+
"Authorization: [REDACTED]"
|
|
266
|
+
)
|
|
267
|
+
.replace(
|
|
268
|
+
/(api(?:[_-]|\s)?key|apikey|access(?:[_-]|\s)?token|token|secret|password|key)(\s*["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s&"']+)/gi,
|
|
269
|
+
"$1$2[REDACTED]"
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Safely extract a serializable, sanitized message/status/code from an unknown
|
|
275
|
+
* caught error (Axios error, native Error, or wrapped ApiError with `.cause`)
|
|
276
|
+
* so it can be attached to failure tracking events without leaking secrets.
|
|
277
|
+
*/
|
|
278
|
+
export const extractErrorDetails = (
|
|
279
|
+
error: unknown
|
|
280
|
+
): { errorMessage: string; errorStatus?: number; errorCode?: string } => {
|
|
281
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
282
|
+
const errorMessage = sanitizeErrorMessage(rawMessage);
|
|
283
|
+
const cause = (error as any)?.cause;
|
|
284
|
+
const errorStatus =
|
|
285
|
+
(error as any)?.response?.status ??
|
|
286
|
+
(error as any)?.status ??
|
|
287
|
+
cause?.response?.status ??
|
|
288
|
+
undefined;
|
|
289
|
+
const errorCode = [error, cause]
|
|
290
|
+
.map(source => (source as any)?.code)
|
|
291
|
+
.find((code): code is string => typeof code === "string");
|
|
292
|
+
return { errorMessage, errorStatus, errorCode };
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Build a serializable, sanitized error object from an unknown caught error so
|
|
297
|
+
* it can be handed to app-injected error/logging sinks (`onError`) without
|
|
298
|
+
* exposing raw messages, responses, or causes that may carry credentials.
|
|
299
|
+
*/
|
|
300
|
+
export const sanitizeError = (
|
|
301
|
+
error: unknown
|
|
302
|
+
): { name: string; message: string; status?: number; code?: string } => {
|
|
303
|
+
const { errorMessage, errorStatus, errorCode } = extractErrorDetails(error);
|
|
304
|
+
return {
|
|
305
|
+
name: error instanceof Error ? error.name : "Error",
|
|
306
|
+
message: errorMessage,
|
|
307
|
+
...(errorStatus !== undefined ? { status: errorStatus } : {}),
|
|
308
|
+
...(errorCode !== undefined ? { code: errorCode } : {}),
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
|
|
255
312
|
export { constants, isFormField, isConditional };
|