@sundaeswap/sprinkles 0.4.0 → 0.6.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 +22 -8
- package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +37 -46
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/field-utils.test.js +170 -0
- package/dist/cjs/Sprinkle/__tests__/field-utils.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +283 -81
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/formatting.test.js +97 -0
- package/dist/cjs/Sprinkle/__tests__/formatting.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +97 -7
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +30 -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 +427 -438
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/menus/array-menu.js +195 -0
- package/dist/cjs/Sprinkle/menus/array-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/field-menu.js +161 -0
- package/dist/cjs/Sprinkle/menus/field-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/index.js +33 -0
- package/dist/cjs/Sprinkle/menus/index.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/object-menu.js +324 -0
- package/dist/cjs/Sprinkle/menus/object-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/prompts.js +459 -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 +89 -0
- package/dist/cjs/Sprinkle/type-guards.js.map +1 -0
- package/dist/cjs/Sprinkle/types.js +73 -0
- package/dist/cjs/Sprinkle/types.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/field-utils.js +154 -0
- package/dist/cjs/Sprinkle/utils/field-utils.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/formatting.js +126 -0
- package/dist/cjs/Sprinkle/utils/formatting.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/index.js +56 -0
- package/dist/cjs/Sprinkle/utils/index.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 +22 -8
- package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js +37 -46
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/field-utils.test.js +168 -0
- package/dist/esm/Sprinkle/__tests__/field-utils.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +284 -82
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/formatting.test.js +95 -0
- package/dist/esm/Sprinkle/__tests__/formatting.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js +98 -8
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +30 -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 +248 -425
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/menus/array-menu.js +190 -0
- package/dist/esm/Sprinkle/menus/array-menu.js.map +1 -0
- package/dist/esm/Sprinkle/menus/field-menu.js +155 -0
- package/dist/esm/Sprinkle/menus/field-menu.js.map +1 -0
- package/dist/esm/Sprinkle/menus/index.js +8 -0
- package/dist/esm/Sprinkle/menus/index.js.map +1 -0
- package/dist/esm/Sprinkle/menus/object-menu.js +318 -0
- package/dist/esm/Sprinkle/menus/object-menu.js.map +1 -0
- package/dist/esm/Sprinkle/prompts.js +443 -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 +66 -0
- package/dist/esm/Sprinkle/type-guards.js.map +1 -0
- package/dist/esm/Sprinkle/types.js +66 -0
- package/dist/esm/Sprinkle/types.js.map +1 -0
- package/dist/esm/Sprinkle/utils/field-utils.js +145 -0
- package/dist/esm/Sprinkle/utils/field-utils.js.map +1 -0
- package/dist/esm/Sprinkle/utils/formatting.js +118 -0
- package/dist/esm/Sprinkle/utils/formatting.js.map +1 -0
- package/dist/esm/Sprinkle/utils/index.js +7 -0
- package/dist/esm/Sprinkle/utils/index.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 +17 -177
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/menus/array-menu.d.ts +31 -0
- package/dist/types/Sprinkle/menus/array-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/field-menu.d.ts +34 -0
- package/dist/types/Sprinkle/menus/field-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/index.d.ts +10 -0
- package/dist/types/Sprinkle/menus/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/object-menu.d.ts +34 -0
- package/dist/types/Sprinkle/menus/object-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/prompts.d.ts +119 -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 +45 -0
- package/dist/types/Sprinkle/type-guards.d.ts.map +1 -0
- package/dist/types/Sprinkle/types.d.ts +115 -0
- package/dist/types/Sprinkle/types.d.ts.map +1 -0
- package/dist/types/Sprinkle/utils/field-utils.d.ts +47 -0
- package/dist/types/Sprinkle/utils/field-utils.d.ts.map +1 -0
- package/dist/types/Sprinkle/utils/formatting.d.ts +30 -0
- package/dist/types/Sprinkle/utils/formatting.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 +23 -8
- package/src/Sprinkle/__tests__/enhancements.test.ts +34 -47
- package/src/Sprinkle/__tests__/field-utils.test.ts +191 -0
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +301 -86
- package/src/Sprinkle/__tests__/formatting.test.ts +115 -0
- package/src/Sprinkle/__tests__/show-menu.test.ts +102 -8
- package/src/Sprinkle/__tests__/tx-dialog.test.ts +30 -0
- package/src/Sprinkle/encryption.ts +130 -0
- package/src/Sprinkle/index.ts +368 -598
- package/src/Sprinkle/menus/array-menu.ts +191 -0
- package/src/Sprinkle/menus/field-menu.ts +145 -0
- package/src/Sprinkle/menus/index.ts +12 -0
- package/src/Sprinkle/menus/object-menu.ts +336 -0
- package/src/Sprinkle/prompts.ts +551 -0
- package/src/Sprinkle/schemas.ts +111 -0
- package/src/Sprinkle/tx-dialog.ts +100 -0
- package/src/Sprinkle/type-guards.ts +93 -0
- package/src/Sprinkle/types.ts +116 -0
- package/src/Sprinkle/utils/field-utils.ts +158 -0
- package/src/Sprinkle/utils/formatting.ts +127 -0
- package/src/Sprinkle/utils/index.ts +17 -0
- package/src/Sprinkle/wallet.ts +133 -0
|
@@ -1,15 +1,26 @@
|
|
|
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
|
+
searchCancellable: mock(),
|
|
21
|
+
select: mockSelectCancellable,
|
|
22
|
+
}));
|
|
23
|
+
|
|
13
24
|
describe("showMenu", () => {
|
|
14
25
|
let sprinkle: Sprinkle<any>;
|
|
15
26
|
|
|
@@ -18,11 +29,12 @@ describe("showMenu", () => {
|
|
|
18
29
|
sprinkle = withProfile(new Sprinkle(schema, "/tmp/test"));
|
|
19
30
|
sprinkle.settings = { name: "test" } as any;
|
|
20
31
|
mockSelect.mockClear();
|
|
32
|
+
mockSelectCancellable.mockClear();
|
|
21
33
|
mockInput.mockClear();
|
|
22
34
|
});
|
|
23
35
|
|
|
24
36
|
test("exits when Exit is selected on main menu", async () => {
|
|
25
|
-
|
|
37
|
+
mockSelectCancellable.mockResolvedValueOnce(-1);
|
|
26
38
|
|
|
27
39
|
const menu: IMenu<any> = {
|
|
28
40
|
title: "Test Menu",
|
|
@@ -40,7 +52,7 @@ describe("showMenu", () => {
|
|
|
40
52
|
test("executes action and re-shows menu", async () => {
|
|
41
53
|
const actionFn = mock(async () => {});
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
mockSelectCancellable.mockResolvedValueOnce(0).mockResolvedValueOnce(-1);
|
|
44
56
|
|
|
45
57
|
const menu: IMenu<any> = {
|
|
46
58
|
title: "Test Menu",
|
|
@@ -57,7 +69,7 @@ describe("showMenu", () => {
|
|
|
57
69
|
});
|
|
58
70
|
|
|
59
71
|
test("main menu includes Settings & Profiles submenu and Exit", async () => {
|
|
60
|
-
|
|
72
|
+
mockSelectCancellable.mockResolvedValueOnce(-1);
|
|
61
73
|
|
|
62
74
|
const menu: IMenu<any> = {
|
|
63
75
|
title: "Test",
|
|
@@ -66,7 +78,7 @@ describe("showMenu", () => {
|
|
|
66
78
|
|
|
67
79
|
await sprinkle.showMenu(menu);
|
|
68
80
|
|
|
69
|
-
const choices =
|
|
81
|
+
const choices = mockSelectCancellable.mock.calls[0][0].choices;
|
|
70
82
|
const names = choices.map((c: any) => c.name);
|
|
71
83
|
expect(names).toContain("Settings & Profiles");
|
|
72
84
|
expect(names).toContain("Exit");
|
|
@@ -76,7 +88,7 @@ describe("showMenu", () => {
|
|
|
76
88
|
});
|
|
77
89
|
|
|
78
90
|
test("submenu includes Back instead of Exit", async () => {
|
|
79
|
-
|
|
91
|
+
mockSelectCancellable
|
|
80
92
|
.mockResolvedValueOnce(0)
|
|
81
93
|
.mockResolvedValueOnce(-1)
|
|
82
94
|
.mockResolvedValueOnce(-1);
|
|
@@ -93,7 +105,7 @@ describe("showMenu", () => {
|
|
|
93
105
|
|
|
94
106
|
await sprinkle.showMenu(menu);
|
|
95
107
|
|
|
96
|
-
const subChoices =
|
|
108
|
+
const subChoices = mockSelectCancellable.mock.calls[1][0].choices;
|
|
97
109
|
const subNames = subChoices.map((c: any) => c.name);
|
|
98
110
|
expect(subNames).toContain("Back");
|
|
99
111
|
expect(subNames).not.toContain("Exit");
|
|
@@ -102,7 +114,7 @@ describe("showMenu", () => {
|
|
|
102
114
|
test("action returning sprinkle instance saves settings", async () => {
|
|
103
115
|
sprinkle.settings = { name: "original" } as any;
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
mockSelectCancellable.mockResolvedValueOnce(0).mockResolvedValueOnce(-1);
|
|
106
118
|
|
|
107
119
|
const menu: IMenu<any> = {
|
|
108
120
|
title: "Test",
|
|
@@ -120,4 +132,86 @@ describe("showMenu", () => {
|
|
|
120
132
|
await sprinkle.showMenu(menu);
|
|
121
133
|
expect(sprinkle.settings).toEqual({ name: "updated" });
|
|
122
134
|
});
|
|
135
|
+
|
|
136
|
+
test("Settings submenu includes View settings option", async () => {
|
|
137
|
+
// Select "Settings & Profiles" (-5), then "Back" (-1), then "Exit" (-1)
|
|
138
|
+
mockSelectCancellable
|
|
139
|
+
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
140
|
+
.mockResolvedValueOnce(-1) // Back
|
|
141
|
+
.mockResolvedValueOnce(-1); // Exit
|
|
142
|
+
|
|
143
|
+
const menu: IMenu<any> = {
|
|
144
|
+
title: "Test",
|
|
145
|
+
items: [{ title: "Action", action: async () => {} }],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await sprinkle.showMenu(menu);
|
|
149
|
+
|
|
150
|
+
// Second call is the settings submenu
|
|
151
|
+
const settingsChoices = mockSelectCancellable.mock.calls[1][0].choices;
|
|
152
|
+
const settingsNames = settingsChoices.map((c: any) => c.name);
|
|
153
|
+
expect(settingsNames).toContain("View settings");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("View settings displays masked settings", async () => {
|
|
157
|
+
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
158
|
+
|
|
159
|
+
// Select "Settings & Profiles" (-5), then "View settings" (0),
|
|
160
|
+
// then "Continue" for the press Enter prompt, then "Back" (-1), then "Exit" (-1)
|
|
161
|
+
mockSelectCancellable
|
|
162
|
+
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
163
|
+
.mockResolvedValueOnce(0) // View settings (first item)
|
|
164
|
+
.mockResolvedValueOnce("continue") // Press Enter to continue
|
|
165
|
+
.mockResolvedValueOnce(-1) // Back
|
|
166
|
+
.mockResolvedValueOnce(-1); // Exit
|
|
167
|
+
|
|
168
|
+
const menu: IMenu<any> = {
|
|
169
|
+
title: "Test",
|
|
170
|
+
items: [{ title: "Action", action: async () => {} }],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
await sprinkle.showMenu(menu);
|
|
174
|
+
|
|
175
|
+
// Should have called console.log with the settings (find the JSON output among breadcrumbs)
|
|
176
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
177
|
+
const jsonOutput = consoleSpy.mock.calls.find((call: any[]) =>
|
|
178
|
+
call[0] && typeof call[0] === "string" && call[0].includes('"name"')
|
|
179
|
+
);
|
|
180
|
+
expect(jsonOutput).toBeDefined();
|
|
181
|
+
|
|
182
|
+
consoleSpy.mockRestore();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("Create profile restores state when FillInStruct is cancelled", async () => {
|
|
186
|
+
// Capture original state
|
|
187
|
+
const originalProfileId = sprinkle.currentProfile?.id;
|
|
188
|
+
const originalSettings = { ...sprinkle.settings };
|
|
189
|
+
|
|
190
|
+
// Settings menu indices: 0=View, 1=Edit, 2=Switch, 3=Create new profile
|
|
191
|
+
mockSelectCancellable
|
|
192
|
+
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
193
|
+
.mockResolvedValueOnce(3); // Create new profile
|
|
194
|
+
|
|
195
|
+
// promptProfileMeta: name then description
|
|
196
|
+
mockInputCancellable
|
|
197
|
+
.mockResolvedValueOnce("New Profile") // Profile name
|
|
198
|
+
.mockResolvedValueOnce("") // Profile description
|
|
199
|
+
.mockResolvedValueOnce(null); // Cancel during FillInStruct (name field)
|
|
200
|
+
|
|
201
|
+
// After cancellation, back out of menus
|
|
202
|
+
mockSelectCancellable
|
|
203
|
+
.mockResolvedValueOnce(-1) // Back from settings
|
|
204
|
+
.mockResolvedValueOnce(-1); // Exit
|
|
205
|
+
|
|
206
|
+
const menu: IMenu<any> = {
|
|
207
|
+
title: "Test",
|
|
208
|
+
items: [{ title: "Action", action: async () => {} }],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
await sprinkle.showMenu(menu);
|
|
212
|
+
|
|
213
|
+
// State should be restored to original
|
|
214
|
+
expect(sprinkle.currentProfile?.id).toBe(originalProfileId);
|
|
215
|
+
expect(sprinkle.settings).toEqual(originalSettings);
|
|
216
|
+
});
|
|
123
217
|
});
|
|
@@ -29,6 +29,36 @@ 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
|
+
searchCancellable: mock(async () => "result"),
|
|
52
|
+
select: mock(async (opts: any) => {
|
|
53
|
+
selectCalls.push(opts);
|
|
54
|
+
const response = selectResponses.shift();
|
|
55
|
+
if (response === undefined) {
|
|
56
|
+
return "cancel";
|
|
57
|
+
}
|
|
58
|
+
return response;
|
|
59
|
+
}),
|
|
60
|
+
}));
|
|
61
|
+
|
|
32
62
|
// Mock clipboardy
|
|
33
63
|
let clipboardContent = "";
|
|
34
64
|
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
|
+
}
|