@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.
Files changed (140) hide show
  1. package/dist/cjs/Sprinkle/__tests__/encryption.test.js +22 -8
  2. package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
  3. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +37 -46
  4. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  5. package/dist/cjs/Sprinkle/__tests__/field-utils.test.js +170 -0
  6. package/dist/cjs/Sprinkle/__tests__/field-utils.test.js.map +1 -0
  7. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +283 -81
  8. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  9. package/dist/cjs/Sprinkle/__tests__/formatting.test.js +97 -0
  10. package/dist/cjs/Sprinkle/__tests__/formatting.test.js.map +1 -0
  11. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +97 -7
  12. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  13. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +30 -0
  14. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  15. package/dist/cjs/Sprinkle/encryption.js +131 -0
  16. package/dist/cjs/Sprinkle/encryption.js.map +1 -0
  17. package/dist/cjs/Sprinkle/index.js +427 -438
  18. package/dist/cjs/Sprinkle/index.js.map +1 -1
  19. package/dist/cjs/Sprinkle/menus/array-menu.js +195 -0
  20. package/dist/cjs/Sprinkle/menus/array-menu.js.map +1 -0
  21. package/dist/cjs/Sprinkle/menus/field-menu.js +161 -0
  22. package/dist/cjs/Sprinkle/menus/field-menu.js.map +1 -0
  23. package/dist/cjs/Sprinkle/menus/index.js +33 -0
  24. package/dist/cjs/Sprinkle/menus/index.js.map +1 -0
  25. package/dist/cjs/Sprinkle/menus/object-menu.js +324 -0
  26. package/dist/cjs/Sprinkle/menus/object-menu.js.map +1 -0
  27. package/dist/cjs/Sprinkle/prompts.js +459 -0
  28. package/dist/cjs/Sprinkle/prompts.js.map +1 -0
  29. package/dist/cjs/Sprinkle/schemas.js +97 -0
  30. package/dist/cjs/Sprinkle/schemas.js.map +1 -0
  31. package/dist/cjs/Sprinkle/tx-dialog.js +101 -0
  32. package/dist/cjs/Sprinkle/tx-dialog.js.map +1 -0
  33. package/dist/cjs/Sprinkle/type-guards.js +89 -0
  34. package/dist/cjs/Sprinkle/type-guards.js.map +1 -0
  35. package/dist/cjs/Sprinkle/types.js +73 -0
  36. package/dist/cjs/Sprinkle/types.js.map +1 -0
  37. package/dist/cjs/Sprinkle/utils/field-utils.js +154 -0
  38. package/dist/cjs/Sprinkle/utils/field-utils.js.map +1 -0
  39. package/dist/cjs/Sprinkle/utils/formatting.js +126 -0
  40. package/dist/cjs/Sprinkle/utils/formatting.js.map +1 -0
  41. package/dist/cjs/Sprinkle/utils/index.js +56 -0
  42. package/dist/cjs/Sprinkle/utils/index.js.map +1 -0
  43. package/dist/cjs/Sprinkle/wallet.js +98 -0
  44. package/dist/cjs/Sprinkle/wallet.js.map +1 -0
  45. package/dist/esm/Sprinkle/__tests__/encryption.test.js +22 -8
  46. package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
  47. package/dist/esm/Sprinkle/__tests__/enhancements.test.js +37 -46
  48. package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  49. package/dist/esm/Sprinkle/__tests__/field-utils.test.js +168 -0
  50. package/dist/esm/Sprinkle/__tests__/field-utils.test.js.map +1 -0
  51. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +284 -82
  52. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  53. package/dist/esm/Sprinkle/__tests__/formatting.test.js +95 -0
  54. package/dist/esm/Sprinkle/__tests__/formatting.test.js.map +1 -0
  55. package/dist/esm/Sprinkle/__tests__/show-menu.test.js +98 -8
  56. package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  57. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +30 -0
  58. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  59. package/dist/esm/Sprinkle/encryption.js +117 -0
  60. package/dist/esm/Sprinkle/encryption.js.map +1 -0
  61. package/dist/esm/Sprinkle/index.js +248 -425
  62. package/dist/esm/Sprinkle/index.js.map +1 -1
  63. package/dist/esm/Sprinkle/menus/array-menu.js +190 -0
  64. package/dist/esm/Sprinkle/menus/array-menu.js.map +1 -0
  65. package/dist/esm/Sprinkle/menus/field-menu.js +155 -0
  66. package/dist/esm/Sprinkle/menus/field-menu.js.map +1 -0
  67. package/dist/esm/Sprinkle/menus/index.js +8 -0
  68. package/dist/esm/Sprinkle/menus/index.js.map +1 -0
  69. package/dist/esm/Sprinkle/menus/object-menu.js +318 -0
  70. package/dist/esm/Sprinkle/menus/object-menu.js.map +1 -0
  71. package/dist/esm/Sprinkle/prompts.js +443 -0
  72. package/dist/esm/Sprinkle/prompts.js.map +1 -0
  73. package/dist/esm/Sprinkle/schemas.js +91 -0
  74. package/dist/esm/Sprinkle/schemas.js.map +1 -0
  75. package/dist/esm/Sprinkle/tx-dialog.js +90 -0
  76. package/dist/esm/Sprinkle/tx-dialog.js.map +1 -0
  77. package/dist/esm/Sprinkle/type-guards.js +66 -0
  78. package/dist/esm/Sprinkle/type-guards.js.map +1 -0
  79. package/dist/esm/Sprinkle/types.js +66 -0
  80. package/dist/esm/Sprinkle/types.js.map +1 -0
  81. package/dist/esm/Sprinkle/utils/field-utils.js +145 -0
  82. package/dist/esm/Sprinkle/utils/field-utils.js.map +1 -0
  83. package/dist/esm/Sprinkle/utils/formatting.js +118 -0
  84. package/dist/esm/Sprinkle/utils/formatting.js.map +1 -0
  85. package/dist/esm/Sprinkle/utils/index.js +7 -0
  86. package/dist/esm/Sprinkle/utils/index.js.map +1 -0
  87. package/dist/esm/Sprinkle/wallet.js +90 -0
  88. package/dist/esm/Sprinkle/wallet.js.map +1 -0
  89. package/dist/types/Sprinkle/encryption.d.ts +43 -0
  90. package/dist/types/Sprinkle/encryption.d.ts.map +1 -0
  91. package/dist/types/Sprinkle/index.d.ts +17 -177
  92. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  93. package/dist/types/Sprinkle/menus/array-menu.d.ts +31 -0
  94. package/dist/types/Sprinkle/menus/array-menu.d.ts.map +1 -0
  95. package/dist/types/Sprinkle/menus/field-menu.d.ts +34 -0
  96. package/dist/types/Sprinkle/menus/field-menu.d.ts.map +1 -0
  97. package/dist/types/Sprinkle/menus/index.d.ts +10 -0
  98. package/dist/types/Sprinkle/menus/index.d.ts.map +1 -0
  99. package/dist/types/Sprinkle/menus/object-menu.d.ts +34 -0
  100. package/dist/types/Sprinkle/menus/object-menu.d.ts.map +1 -0
  101. package/dist/types/Sprinkle/prompts.d.ts +119 -0
  102. package/dist/types/Sprinkle/prompts.d.ts.map +1 -0
  103. package/dist/types/Sprinkle/schemas.d.ts +125 -0
  104. package/dist/types/Sprinkle/schemas.d.ts.map +1 -0
  105. package/dist/types/Sprinkle/tx-dialog.d.ts +37 -0
  106. package/dist/types/Sprinkle/tx-dialog.d.ts.map +1 -0
  107. package/dist/types/Sprinkle/type-guards.d.ts +45 -0
  108. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -0
  109. package/dist/types/Sprinkle/types.d.ts +115 -0
  110. package/dist/types/Sprinkle/types.d.ts.map +1 -0
  111. package/dist/types/Sprinkle/utils/field-utils.d.ts +47 -0
  112. package/dist/types/Sprinkle/utils/field-utils.d.ts.map +1 -0
  113. package/dist/types/Sprinkle/utils/formatting.d.ts +30 -0
  114. package/dist/types/Sprinkle/utils/formatting.d.ts.map +1 -0
  115. package/dist/types/Sprinkle/wallet.d.ts +27 -0
  116. package/dist/types/Sprinkle/wallet.d.ts.map +1 -0
  117. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  118. package/package.json +1 -1
  119. package/src/Sprinkle/__tests__/encryption.test.ts +23 -8
  120. package/src/Sprinkle/__tests__/enhancements.test.ts +34 -47
  121. package/src/Sprinkle/__tests__/field-utils.test.ts +191 -0
  122. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +301 -86
  123. package/src/Sprinkle/__tests__/formatting.test.ts +115 -0
  124. package/src/Sprinkle/__tests__/show-menu.test.ts +102 -8
  125. package/src/Sprinkle/__tests__/tx-dialog.test.ts +30 -0
  126. package/src/Sprinkle/encryption.ts +130 -0
  127. package/src/Sprinkle/index.ts +368 -598
  128. package/src/Sprinkle/menus/array-menu.ts +191 -0
  129. package/src/Sprinkle/menus/field-menu.ts +145 -0
  130. package/src/Sprinkle/menus/index.ts +12 -0
  131. package/src/Sprinkle/menus/object-menu.ts +336 -0
  132. package/src/Sprinkle/prompts.ts +551 -0
  133. package/src/Sprinkle/schemas.ts +111 -0
  134. package/src/Sprinkle/tx-dialog.ts +100 -0
  135. package/src/Sprinkle/type-guards.ts +93 -0
  136. package/src/Sprinkle/types.ts +116 -0
  137. package/src/Sprinkle/utils/field-utils.ts +158 -0
  138. package/src/Sprinkle/utils/formatting.ts +127 -0
  139. package/src/Sprinkle/utils/index.ts +17 -0
  140. 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
- mockSelect.mockResolvedValueOnce(-1);
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
- mockSelect.mockResolvedValueOnce(0).mockResolvedValueOnce(-1);
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
- mockSelect.mockResolvedValueOnce(-1);
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 = mockSelect.mock.calls[0][0].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
- mockSelect
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 = mockSelect.mock.calls[1][0].choices;
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
- mockSelect.mockResolvedValueOnce(0).mockResolvedValueOnce(-1);
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
+ }