@sundaeswap/sprinkles 0.3.0 → 0.5.0
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/cjs/Sprinkle/__tests__/encryption.test.js +20 -8
- package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +41 -16
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +85 -38
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js +120 -0
- package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +93 -7
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +21 -0
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
- package/dist/cjs/Sprinkle/encryption.js +131 -0
- package/dist/cjs/Sprinkle/encryption.js.map +1 -0
- package/dist/cjs/Sprinkle/index.js +318 -352
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/prompts.js +393 -0
- package/dist/cjs/Sprinkle/prompts.js.map +1 -0
- package/dist/cjs/Sprinkle/schemas.js +97 -0
- package/dist/cjs/Sprinkle/schemas.js.map +1 -0
- package/dist/cjs/Sprinkle/tx-dialog.js +101 -0
- package/dist/cjs/Sprinkle/tx-dialog.js.map +1 -0
- package/dist/cjs/Sprinkle/type-guards.js +42 -0
- package/dist/cjs/Sprinkle/type-guards.js.map +1 -0
- package/dist/cjs/Sprinkle/types.js +49 -0
- package/dist/cjs/Sprinkle/types.js.map +1 -0
- package/dist/cjs/Sprinkle/wallet.js +98 -0
- package/dist/cjs/Sprinkle/wallet.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/encryption.test.js +20 -8
- package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js +41 -16
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +85 -38
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js +120 -0
- package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js +94 -8
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +21 -0
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
- package/dist/esm/Sprinkle/encryption.js +117 -0
- package/dist/esm/Sprinkle/encryption.js.map +1 -0
- package/dist/esm/Sprinkle/index.js +172 -337
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/prompts.js +385 -0
- package/dist/esm/Sprinkle/prompts.js.map +1 -0
- package/dist/esm/Sprinkle/schemas.js +91 -0
- package/dist/esm/Sprinkle/schemas.js.map +1 -0
- package/dist/esm/Sprinkle/tx-dialog.js +90 -0
- package/dist/esm/Sprinkle/tx-dialog.js.map +1 -0
- package/dist/esm/Sprinkle/type-guards.js +24 -0
- package/dist/esm/Sprinkle/type-guards.js.map +1 -0
- package/dist/esm/Sprinkle/types.js +42 -0
- package/dist/esm/Sprinkle/types.js.map +1 -0
- package/dist/esm/Sprinkle/wallet.js +90 -0
- package/dist/esm/Sprinkle/wallet.js.map +1 -0
- package/dist/types/Sprinkle/encryption.d.ts +43 -0
- package/dist/types/Sprinkle/encryption.d.ts.map +1 -0
- package/dist/types/Sprinkle/index.d.ts +13 -174
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/prompts.d.ts +94 -0
- package/dist/types/Sprinkle/prompts.d.ts.map +1 -0
- package/dist/types/Sprinkle/schemas.d.ts +125 -0
- package/dist/types/Sprinkle/schemas.d.ts.map +1 -0
- package/dist/types/Sprinkle/tx-dialog.d.ts +37 -0
- package/dist/types/Sprinkle/tx-dialog.d.ts.map +1 -0
- package/dist/types/Sprinkle/type-guards.d.ts +22 -0
- package/dist/types/Sprinkle/type-guards.d.ts.map +1 -0
- package/dist/types/Sprinkle/types.d.ts +62 -0
- package/dist/types/Sprinkle/types.d.ts.map +1 -0
- package/dist/types/Sprinkle/wallet.d.ts +27 -0
- package/dist/types/Sprinkle/wallet.d.ts.map +1 -0
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/encryption.test.ts +21 -8
- package/src/Sprinkle/__tests__/enhancements.test.ts +41 -15
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +104 -38
- package/src/Sprinkle/__tests__/settings-persistence.test.ts +108 -0
- package/src/Sprinkle/__tests__/show-menu.test.ts +96 -8
- package/src/Sprinkle/__tests__/tx-dialog.test.ts +21 -0
- package/src/Sprinkle/encryption.ts +130 -0
- package/src/Sprinkle/index.ts +265 -478
- package/src/Sprinkle/prompts.ts +481 -0
- package/src/Sprinkle/schemas.ts +111 -0
- package/src/Sprinkle/tx-dialog.ts +100 -0
- package/src/Sprinkle/type-guards.ts +51 -0
- package/src/Sprinkle/types.ts +73 -0
- package/src/Sprinkle/wallet.ts +133 -0
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { describe, expect, test, mock, beforeEach } from "bun:test";
|
|
1
|
+
import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test";
|
|
2
2
|
import { Sprinkle, Type, type IMenu } from "../index.js";
|
|
3
3
|
import { withProfile } from "./test-helpers.js";
|
|
4
4
|
|
|
5
5
|
const mockSelect = mock();
|
|
6
6
|
const mockInput = mock();
|
|
7
|
+
const mockSelectCancellable = mock();
|
|
8
|
+
const mockInputCancellable = mock();
|
|
7
9
|
|
|
8
10
|
mock.module("@inquirer/prompts", () => ({
|
|
9
11
|
select: mockSelect,
|
|
10
12
|
input: mockInput,
|
|
11
13
|
}));
|
|
12
14
|
|
|
15
|
+
mock.module("../prompts.js", () => ({
|
|
16
|
+
selectCancellable: mockSelectCancellable,
|
|
17
|
+
inputCancellable: mockInputCancellable,
|
|
18
|
+
passwordCancellable: mock(),
|
|
19
|
+
confirmCancellable: mock(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
13
22
|
describe("showMenu", () => {
|
|
14
23
|
let sprinkle: Sprinkle<any>;
|
|
15
24
|
|
|
@@ -18,11 +27,12 @@ describe("showMenu", () => {
|
|
|
18
27
|
sprinkle = withProfile(new Sprinkle(schema, "/tmp/test"));
|
|
19
28
|
sprinkle.settings = { name: "test" } as any;
|
|
20
29
|
mockSelect.mockClear();
|
|
30
|
+
mockSelectCancellable.mockClear();
|
|
21
31
|
mockInput.mockClear();
|
|
22
32
|
});
|
|
23
33
|
|
|
24
34
|
test("exits when Exit is selected on main menu", async () => {
|
|
25
|
-
|
|
35
|
+
mockSelectCancellable.mockResolvedValueOnce(-1);
|
|
26
36
|
|
|
27
37
|
const menu: IMenu<any> = {
|
|
28
38
|
title: "Test Menu",
|
|
@@ -40,7 +50,7 @@ describe("showMenu", () => {
|
|
|
40
50
|
test("executes action and re-shows menu", async () => {
|
|
41
51
|
const actionFn = mock(async () => {});
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
mockSelectCancellable.mockResolvedValueOnce(0).mockResolvedValueOnce(-1);
|
|
44
54
|
|
|
45
55
|
const menu: IMenu<any> = {
|
|
46
56
|
title: "Test Menu",
|
|
@@ -57,7 +67,7 @@ describe("showMenu", () => {
|
|
|
57
67
|
});
|
|
58
68
|
|
|
59
69
|
test("main menu includes Settings & Profiles submenu and Exit", async () => {
|
|
60
|
-
|
|
70
|
+
mockSelectCancellable.mockResolvedValueOnce(-1);
|
|
61
71
|
|
|
62
72
|
const menu: IMenu<any> = {
|
|
63
73
|
title: "Test",
|
|
@@ -66,7 +76,7 @@ describe("showMenu", () => {
|
|
|
66
76
|
|
|
67
77
|
await sprinkle.showMenu(menu);
|
|
68
78
|
|
|
69
|
-
const choices =
|
|
79
|
+
const choices = mockSelectCancellable.mock.calls[0][0].choices;
|
|
70
80
|
const names = choices.map((c: any) => c.name);
|
|
71
81
|
expect(names).toContain("Settings & Profiles");
|
|
72
82
|
expect(names).toContain("Exit");
|
|
@@ -76,7 +86,7 @@ describe("showMenu", () => {
|
|
|
76
86
|
});
|
|
77
87
|
|
|
78
88
|
test("submenu includes Back instead of Exit", async () => {
|
|
79
|
-
|
|
89
|
+
mockSelectCancellable
|
|
80
90
|
.mockResolvedValueOnce(0)
|
|
81
91
|
.mockResolvedValueOnce(-1)
|
|
82
92
|
.mockResolvedValueOnce(-1);
|
|
@@ -93,7 +103,7 @@ describe("showMenu", () => {
|
|
|
93
103
|
|
|
94
104
|
await sprinkle.showMenu(menu);
|
|
95
105
|
|
|
96
|
-
const subChoices =
|
|
106
|
+
const subChoices = mockSelectCancellable.mock.calls[1][0].choices;
|
|
97
107
|
const subNames = subChoices.map((c: any) => c.name);
|
|
98
108
|
expect(subNames).toContain("Back");
|
|
99
109
|
expect(subNames).not.toContain("Exit");
|
|
@@ -102,7 +112,7 @@ describe("showMenu", () => {
|
|
|
102
112
|
test("action returning sprinkle instance saves settings", async () => {
|
|
103
113
|
sprinkle.settings = { name: "original" } as any;
|
|
104
114
|
|
|
105
|
-
|
|
115
|
+
mockSelectCancellable.mockResolvedValueOnce(0).mockResolvedValueOnce(-1);
|
|
106
116
|
|
|
107
117
|
const menu: IMenu<any> = {
|
|
108
118
|
title: "Test",
|
|
@@ -120,4 +130,82 @@ describe("showMenu", () => {
|
|
|
120
130
|
await sprinkle.showMenu(menu);
|
|
121
131
|
expect(sprinkle.settings).toEqual({ name: "updated" });
|
|
122
132
|
});
|
|
133
|
+
|
|
134
|
+
test("Settings submenu includes View settings option", async () => {
|
|
135
|
+
// Select "Settings & Profiles" (-5), then "Back" (-1), then "Exit" (-1)
|
|
136
|
+
mockSelectCancellable
|
|
137
|
+
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
138
|
+
.mockResolvedValueOnce(-1) // Back
|
|
139
|
+
.mockResolvedValueOnce(-1); // Exit
|
|
140
|
+
|
|
141
|
+
const menu: IMenu<any> = {
|
|
142
|
+
title: "Test",
|
|
143
|
+
items: [{ title: "Action", action: async () => {} }],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await sprinkle.showMenu(menu);
|
|
147
|
+
|
|
148
|
+
// Second call is the settings submenu
|
|
149
|
+
const settingsChoices = mockSelectCancellable.mock.calls[1][0].choices;
|
|
150
|
+
const settingsNames = settingsChoices.map((c: any) => c.name);
|
|
151
|
+
expect(settingsNames).toContain("View settings");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("View settings displays masked settings", async () => {
|
|
155
|
+
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
156
|
+
|
|
157
|
+
// Select "Settings & Profiles" (-5), then "View settings" (0), then "Back" (-1), then "Exit" (-1)
|
|
158
|
+
mockSelectCancellable
|
|
159
|
+
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
160
|
+
.mockResolvedValueOnce(0) // View settings (first item)
|
|
161
|
+
.mockResolvedValueOnce(-1) // Back
|
|
162
|
+
.mockResolvedValueOnce(-1); // Exit
|
|
163
|
+
|
|
164
|
+
const menu: IMenu<any> = {
|
|
165
|
+
title: "Test",
|
|
166
|
+
items: [{ title: "Action", action: async () => {} }],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
await sprinkle.showMenu(menu);
|
|
170
|
+
|
|
171
|
+
// Should have called console.log with the settings
|
|
172
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
173
|
+
const output = consoleSpy.mock.calls[0][0];
|
|
174
|
+
expect(output).toContain("name");
|
|
175
|
+
|
|
176
|
+
consoleSpy.mockRestore();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("Create profile restores state when FillInStruct is cancelled", async () => {
|
|
180
|
+
// Capture original state
|
|
181
|
+
const originalProfileId = sprinkle.currentProfile?.id;
|
|
182
|
+
const originalSettings = { ...sprinkle.settings };
|
|
183
|
+
|
|
184
|
+
// Settings menu indices: 0=View, 1=Edit, 2=Switch, 3=Create new profile
|
|
185
|
+
mockSelectCancellable
|
|
186
|
+
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
187
|
+
.mockResolvedValueOnce(3); // Create new profile
|
|
188
|
+
|
|
189
|
+
// promptProfileMeta: name then description
|
|
190
|
+
mockInputCancellable
|
|
191
|
+
.mockResolvedValueOnce("New Profile") // Profile name
|
|
192
|
+
.mockResolvedValueOnce("") // Profile description
|
|
193
|
+
.mockResolvedValueOnce(null); // Cancel during FillInStruct (name field)
|
|
194
|
+
|
|
195
|
+
// After cancellation, back out of menus
|
|
196
|
+
mockSelectCancellable
|
|
197
|
+
.mockResolvedValueOnce(-1) // Back from settings
|
|
198
|
+
.mockResolvedValueOnce(-1); // Exit
|
|
199
|
+
|
|
200
|
+
const menu: IMenu<any> = {
|
|
201
|
+
title: "Test",
|
|
202
|
+
items: [{ title: "Action", action: async () => {} }],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
await sprinkle.showMenu(menu);
|
|
206
|
+
|
|
207
|
+
// State should be restored to original
|
|
208
|
+
expect(sprinkle.currentProfile?.id).toBe(originalProfileId);
|
|
209
|
+
expect(sprinkle.settings).toEqual(originalSettings);
|
|
210
|
+
});
|
|
123
211
|
});
|
|
@@ -29,6 +29,27 @@ mock.module("@inquirer/prompts", () => ({
|
|
|
29
29
|
search: mock(async () => "result"),
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
+
// Mock cancellable prompts (used by TxDialog and other methods)
|
|
33
|
+
mock.module("../prompts.js", () => ({
|
|
34
|
+
selectCancellable: mock(async (opts: any) => {
|
|
35
|
+
selectCalls.push(opts);
|
|
36
|
+
const response = selectResponses.shift();
|
|
37
|
+
if (response === undefined) {
|
|
38
|
+
return "cancel";
|
|
39
|
+
}
|
|
40
|
+
return response;
|
|
41
|
+
}),
|
|
42
|
+
inputCancellable: mock(async () => {
|
|
43
|
+
const response = inputResponses.shift();
|
|
44
|
+
return response ?? "";
|
|
45
|
+
}),
|
|
46
|
+
passwordCancellable: mock(async () => "secret"),
|
|
47
|
+
confirmCancellable: mock(async () => {
|
|
48
|
+
const response = confirmResponses.shift();
|
|
49
|
+
return response ?? false;
|
|
50
|
+
}),
|
|
51
|
+
}));
|
|
52
|
+
|
|
32
53
|
// Mock clipboardy
|
|
33
54
|
let clipboardContent = "";
|
|
34
55
|
let clipboardShouldFail = false;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption utilities for sensitive field handling.
|
|
3
|
+
* Functions for encrypting, decrypting, and masking sensitive data in settings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
7
|
+
import { isObject, isUnion, isSensitive } from "./type-guards.js";
|
|
8
|
+
import type { IEncryptionOptions } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recursively collect paths to all sensitive fields in a schema.
|
|
12
|
+
*/
|
|
13
|
+
export function collectSensitivePaths(
|
|
14
|
+
type: TSchema,
|
|
15
|
+
prefix: string = "",
|
|
16
|
+
): string[] {
|
|
17
|
+
const paths: string[] = [];
|
|
18
|
+
if (isObject(type)) {
|
|
19
|
+
const fields = type["properties"] as Record<string, TSchema>;
|
|
20
|
+
for (const [field, fieldType] of Object.entries(fields)) {
|
|
21
|
+
const fieldPath = prefix ? `${prefix}.${field}` : field;
|
|
22
|
+
if (isSensitive(fieldType)) {
|
|
23
|
+
paths.push(fieldPath);
|
|
24
|
+
}
|
|
25
|
+
paths.push(...collectSensitivePaths(fieldType, fieldPath));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (isUnion(type)) {
|
|
29
|
+
for (const variant of type.anyOf) {
|
|
30
|
+
paths.push(...collectSensitivePaths(variant, prefix));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return paths;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get a nested value from an object by dot-separated path.
|
|
38
|
+
*/
|
|
39
|
+
export function getNestedValue(obj: any, path: string): unknown {
|
|
40
|
+
return path.split(".").reduce((o, k) => o?.[k], obj);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set a nested value in an object by dot-separated path.
|
|
45
|
+
*/
|
|
46
|
+
export function setNestedValue(obj: any, path: string, value: unknown): void {
|
|
47
|
+
const keys = path.split(".");
|
|
48
|
+
const last = keys.pop()!;
|
|
49
|
+
const parent = keys.reduce((o, k) => o?.[k], obj);
|
|
50
|
+
if (parent && typeof parent === "object") {
|
|
51
|
+
parent[last] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* JSON replacer that converts BigInt to string with 'n' suffix.
|
|
57
|
+
*/
|
|
58
|
+
export function bigIntReplacer(_key: string, value: unknown): unknown {
|
|
59
|
+
return typeof value === "bigint" ? `${value.toString()}n` : value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* JSON reviver that converts strings with 'n' suffix back to BigInt.
|
|
64
|
+
*/
|
|
65
|
+
export function bigIntReviver(_key: string, value: unknown): unknown {
|
|
66
|
+
if (typeof value === "string" && /^\d+n$/.test(value)) {
|
|
67
|
+
return BigInt(value.slice(0, -1));
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deep clone an object, preserving BigInt values.
|
|
74
|
+
*/
|
|
75
|
+
export function cloneWithBigInt<T>(obj: T): T {
|
|
76
|
+
return JSON.parse(JSON.stringify(obj, bigIntReplacer), bigIntReviver);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Encrypt all sensitive fields in a settings object.
|
|
81
|
+
*/
|
|
82
|
+
export function encryptSensitiveFields<T>(
|
|
83
|
+
settings: T,
|
|
84
|
+
type: TSchema,
|
|
85
|
+
encryption: IEncryptionOptions,
|
|
86
|
+
): T {
|
|
87
|
+
const clone = cloneWithBigInt(settings);
|
|
88
|
+
const sensitivePaths = collectSensitivePaths(type);
|
|
89
|
+
for (const p of sensitivePaths) {
|
|
90
|
+
const value = getNestedValue(clone, p);
|
|
91
|
+
if (typeof value === "string" && value.length > 0) {
|
|
92
|
+
setNestedValue(clone, p, encryption.encrypt(value));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return clone;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decrypt all sensitive fields in a settings object.
|
|
100
|
+
*/
|
|
101
|
+
export async function decryptSensitiveFields<T>(
|
|
102
|
+
settings: T,
|
|
103
|
+
type: TSchema,
|
|
104
|
+
encryption: IEncryptionOptions,
|
|
105
|
+
): Promise<T> {
|
|
106
|
+
const clone = cloneWithBigInt(settings);
|
|
107
|
+
const sensitivePaths = collectSensitivePaths(type);
|
|
108
|
+
for (const p of sensitivePaths) {
|
|
109
|
+
const value = getNestedValue(clone, p);
|
|
110
|
+
if (typeof value === "string" && value.length > 0) {
|
|
111
|
+
setNestedValue(clone, p, await encryption.decrypt(value));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return clone;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Mask all sensitive fields in a settings object for display.
|
|
119
|
+
*/
|
|
120
|
+
export function maskSensitiveFields<T>(settings: T, type: TSchema): T {
|
|
121
|
+
const clone = cloneWithBigInt(settings);
|
|
122
|
+
const sensitivePaths = collectSensitivePaths(type);
|
|
123
|
+
for (const p of sensitivePaths) {
|
|
124
|
+
const value = getNestedValue(clone, p);
|
|
125
|
+
if (typeof value === "string" && value.length > 0) {
|
|
126
|
+
setNestedValue(clone, p, "********");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return clone;
|
|
130
|
+
}
|