@memorax/memorax-code 0.1.4 → 0.1.6

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 (87) hide show
  1. package/README.md +8 -19
  2. package/bin/memorax-code-npm-preinstall.mjs +28 -39
  3. package/bin/memorax-code-plugin-postinstall.mjs +23 -1537
  4. package/bin/memorax-code-setup.mjs +1615 -0
  5. package/bin/memorax-code.mjs +289 -5
  6. package/docs/configuration.md +66 -17
  7. package/docs/troubleshooting.md +105 -7
  8. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  9. package/lib/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  10. package/lib/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  11. package/lib/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  12. package/lib/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  13. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  14. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  15. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  16. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  17. package/lib/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  18. package/lib/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  19. package/lib/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  20. package/lib/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  21. package/lib/memorax-code-backend/dist/clients/claude/memory-hook-runtime.js +21 -2
  22. package/lib/memorax-code-backend/dist/clients/codex/memory-hook-runtime.js +23 -3
  23. package/lib/memorax-code-backend/dist/clients/opencode/memory-hook-runtime.js +19 -1
  24. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +17 -16
  25. package/lib/memorax-code-backend/dist/lifecycle/backend/process.js +21 -0
  26. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +81 -22
  27. package/lib/memorax-code-backend/dist/memorax-cli.js +3 -0
  28. package/lib/memorax-code-backend/dist/memory/automatic-retrieval.js +8 -2
  29. package/lib/memorax-code-backend/dist/memory/automatic-writeback.js +13 -1
  30. package/lib/memorax-code-backend/dist/memory/cli.js +10 -0
  31. package/lib/memorax-code-backend/dist/memory/quota-notice.js +208 -0
  32. package/lib/memorax-code-backend/dist/memory/service.js +11 -0
  33. package/lib/memorax-code-backend/dist/provider/memorax/adapter.js +4 -2
  34. package/lib/memorax-code-backend/dist/provider/memorax/http.js +10 -3
  35. package/lib/memorax-code-backend/dist/provider/memorax/quota.js +26 -0
  36. package/lib/memorax-code-backend/package.json +2 -2
  37. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  38. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  39. package/lib/memorax-code-claude-adapter/package.json +1 -1
  40. package/lib/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  41. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  42. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  43. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  44. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  45. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  54. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  59. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  60. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  61. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  62. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  63. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  64. package/lib/memorax-code-codex-adapter/package.json +1 -1
  65. package/lib/memorax-code-codex-adapter/runtime-hooks/memory-skill-reminder.mjs +2 -0
  66. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  67. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  68. package/lib/memorax-code-dsh-adapter/hooks/repo-memory-job.mjs +5 -2
  69. package/lib/memorax-code-dsh-adapter/package.json +1 -1
  70. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  71. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  72. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +158 -7
  73. package/lib/memorax-code-dsh-adapter/src/runtime-state.mjs +21 -0
  74. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  75. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  76. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  77. package/lib/memorax-code-opencode-adapter/src/plugin.mjs +20 -0
  78. package/lib/node-version.mjs +1 -1
  79. package/lib/package-transition.mjs +311 -0
  80. package/lib/resolve-codex-command.mjs +32 -6
  81. package/lib/setup-memory-preferences.mjs +113 -0
  82. package/lib/setup-reconcile.mjs +128 -0
  83. package/lib/trial-plugin-mark.mjs +171 -0
  84. package/lib/trial-provision-client.mjs +307 -0
  85. package/lib/trial-provision-flow.mjs +215 -0
  86. package/lib/trial-setup.mjs +53 -0
  87. package/package.json +2 -2
@@ -0,0 +1,271 @@
1
+ import { createHash } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { withJsonFileLockAsync } from "../config-utils.mjs";
5
+ import { createLinuxSecretServiceBackend } from "./linux-secret-service.mjs";
6
+ import { createMacosKeychainBackend } from "./macos-keychain.mjs";
7
+ import {
8
+ SecureCredentialBackendError,
9
+ secureCredentialBackendError,
10
+ } from "./secure-command.mjs";
11
+ import {
12
+ completeTrialCredentialProvisioning,
13
+ TrialCredentialRecordError,
14
+ parseTrialCredentialRecord,
15
+ serializeTrialCredentialRecord,
16
+ validateTrialCredentialRecord,
17
+ } from "./trial-credential-record.mjs";
18
+ import { createWindowsDpapiBackend } from "./windows-dpapi.mjs";
19
+
20
+ const STORE_BACKEND = "credential-store";
21
+ const HOME_NAMESPACE_DOMAIN = "memorax-code:trial-credential-home:v1\0";
22
+
23
+ export function trialCredentialNamespace(memoraxCodeHome, options) {
24
+ const platform = options?.platform ?? process.platform;
25
+ const resolveHome = options?.resolveHome ?? resolve;
26
+ const home = normalizedHome(memoraxCodeHome, resolveHome);
27
+ const identity = platform === "win32" ? home.toLowerCase() : home;
28
+ return createHash("sha256")
29
+ .update(HOME_NAMESPACE_DOMAIN, "utf8")
30
+ .update(identity, "utf8")
31
+ .digest("hex");
32
+ }
33
+
34
+ export function trialCredentialLockPath(memoraxCodeHome) {
35
+ return join(normalizedHome(memoraxCodeHome, resolve), "runtime", "credentials", "trial-credentials");
36
+ }
37
+
38
+ export function trialCredentialProvisionLockPath(memoraxCodeHome) {
39
+ return join(normalizedHome(memoraxCodeHome, resolve), "runtime", "credentials", "trial-provision");
40
+ }
41
+
42
+ export async function withTrialCredentialProvisionLock(operation, options = {}) {
43
+ if (typeof operation !== "function") {
44
+ throw new TypeError("Trial credential provisioning lock requires an operation");
45
+ }
46
+ const home = resolveStoreHome(options);
47
+ return await withJsonFileLockAsync(
48
+ trialCredentialProvisionLockPath(home),
49
+ operation,
50
+ options.provisionLockOptions ?? options.lockOptions,
51
+ );
52
+ }
53
+
54
+ export function createTrialCredentialStorePort(options = {}) {
55
+ const configured = isRecord(options) ? { ...options } : {};
56
+ const configuredProvisionLockOptions = isRecord(configured.provisionLockOptions)
57
+ ? { ...configured.provisionLockOptions }
58
+ : isRecord(configured.lockOptions)
59
+ ? { ...configured.lockOptions }
60
+ : {};
61
+ return Object.freeze({
62
+ load: () => loadTrialCredentialRecord(configured),
63
+ createIfAbsent: (value) => createTrialCredentialRecordIfAbsent(value, configured),
64
+ complete: (value, metadata) => completeTrialCredentialRecord(value, metadata, configured),
65
+ withProvisionLock(operation, lockOptions = {}) {
66
+ if (!isRecord(lockOptions)) {
67
+ throw new TypeError("Trial credential provisioning lock requires valid options");
68
+ }
69
+ return withTrialCredentialProvisionLock(operation, {
70
+ ...configured,
71
+ provisionLockOptions: {
72
+ ...configuredProvisionLockOptions,
73
+ ...lockOptions,
74
+ },
75
+ });
76
+ },
77
+ });
78
+ }
79
+
80
+ function createTrialCredentialBackend(options = {}) {
81
+ const home = resolveStoreHome(options);
82
+ const platform = options.platform ?? process.platform;
83
+ const namespace = trialCredentialNamespace(home, { platform });
84
+ if (options.backend !== undefined) return validateBackend(options.backend);
85
+ const runtime = isRecord(options.runtime) ? options.runtime : {};
86
+ const backendOptions = {
87
+ ...runtime,
88
+ namespace,
89
+ environment: options.env ?? runtime.environment ?? process.env,
90
+ };
91
+ if (platform === "darwin") return createMacosKeychainBackend(backendOptions);
92
+ if (platform === "linux") return createLinuxSecretServiceBackend(backendOptions);
93
+ if (platform === "win32") return createWindowsDpapiBackend(backendOptions);
94
+ throw secureCredentialBackendError(STORE_BACKEND, "initialize", "backend_unavailable");
95
+ }
96
+
97
+ export async function loadTrialCredentialRecord(options = {}) {
98
+ const backend = createTrialCredentialBackend(options);
99
+ return loadFromBackend(backend, "load");
100
+ }
101
+
102
+ export async function createTrialCredentialRecordIfAbsent(value, options = {}) {
103
+ const candidate = validateTrialCredentialRecord(value);
104
+ if (!isProvisioningSeed(candidate)) {
105
+ throw new TrialCredentialRecordError("invalid_transition");
106
+ }
107
+ const home = resolveStoreHome(options);
108
+ const backend = createTrialCredentialBackend({ ...options, memoraxCodeHome: home });
109
+ return withJsonFileLockAsync(
110
+ trialCredentialLockPath(home),
111
+ async () => {
112
+ const current = await loadFromBackend(backend, "load");
113
+ if (current !== null) {
114
+ return Object.freeze({ record: current, created: false });
115
+ }
116
+ const stored = await saveVerifiedRecord(backend, candidate);
117
+ return Object.freeze({ record: stored, created: true });
118
+ },
119
+ options.lockOptions,
120
+ );
121
+ }
122
+
123
+ export async function completeTrialCredentialRecord(value, metadata, options = {}) {
124
+ const expected = serializeTrialCredentialRecord(value);
125
+ const home = resolveStoreHome(options);
126
+ const backend = createTrialCredentialBackend({ ...options, memoraxCodeHome: home });
127
+ return withJsonFileLockAsync(
128
+ trialCredentialLockPath(home),
129
+ async () => {
130
+ const current = await loadFromBackend(backend, "load");
131
+ if (current === null || serializeTrialCredentialRecord(current) !== expected) {
132
+ throw new TrialCredentialRecordError("invalid_transition");
133
+ }
134
+ let next;
135
+ try {
136
+ next = completeTrialCredentialProvisioning(current, metadata);
137
+ } catch (error) {
138
+ const reason = error instanceof TrialCredentialRecordError
139
+ ? error.reason
140
+ : "invalid_transition";
141
+ throw new TrialCredentialRecordError(reason);
142
+ }
143
+ return saveVerifiedRecord(backend, next);
144
+ },
145
+ options.lockOptions,
146
+ );
147
+ }
148
+
149
+ export async function clearTrialCredentialRecord(options = {}) {
150
+ return await withTrialCredentialProvisionLock(
151
+ () => clearTrialCredentialRecordLocked(options),
152
+ options,
153
+ );
154
+ }
155
+
156
+ async function clearTrialCredentialRecordLocked(options) {
157
+ const home = resolveStoreHome(options);
158
+ const backend = createTrialCredentialBackend({ ...options, memoraxCodeHome: home });
159
+ return withJsonFileLockAsync(
160
+ trialCredentialLockPath(home),
161
+ async () => {
162
+ let deleted;
163
+ try {
164
+ deleted = await backend.delete();
165
+ } catch (error) {
166
+ throw sanitizedBackendError(error, "delete");
167
+ }
168
+ if (typeof deleted !== "boolean") {
169
+ throw secureCredentialBackendError(STORE_BACKEND, "delete", "invalid_response");
170
+ }
171
+ const remaining = await loadSerialized(backend, "verify-delete");
172
+ if (remaining !== null) {
173
+ throw secureCredentialBackendError(STORE_BACKEND, "delete", "storage_failed");
174
+ }
175
+ return { deleted };
176
+ },
177
+ options.lockOptions,
178
+ );
179
+ }
180
+
181
+ function resolveStoreHome(options) {
182
+ const env = isRecord(options?.env) ? options.env : process.env;
183
+ const configured = nonEmptyString(options?.memoraxCodeHome)
184
+ ?? nonEmptyString(env.MEMORAX_CODE_HOME)
185
+ ?? join(homedir(), ".memorax-code");
186
+ return normalizedHome(configured, resolve);
187
+ }
188
+
189
+ function normalizedHome(value, resolveHome) {
190
+ if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
191
+ throw new TypeError("Trial credential store requires a valid MemoraX Code home");
192
+ }
193
+ try {
194
+ const resolved = resolveHome(value.trim());
195
+ if (typeof resolved !== "string" || !resolved || resolved.includes("\0")) {
196
+ throw new TypeError();
197
+ }
198
+ return resolved;
199
+ } catch {
200
+ throw new TypeError("Trial credential store requires a valid MemoraX Code home");
201
+ }
202
+ }
203
+
204
+ function validateBackend(backend) {
205
+ if (!isRecord(backend)
206
+ || typeof backend.load !== "function"
207
+ || typeof backend.save !== "function"
208
+ || typeof backend.delete !== "function") {
209
+ throw secureCredentialBackendError(STORE_BACKEND, "initialize", "backend_unavailable");
210
+ }
211
+ return backend;
212
+ }
213
+
214
+ async function loadFromBackend(backend, operation) {
215
+ const serialized = await loadSerialized(backend, operation);
216
+ return serialized === null ? null : parseTrialCredentialRecord(serialized);
217
+ }
218
+
219
+ async function loadSerialized(backend, operation) {
220
+ let serialized;
221
+ try {
222
+ serialized = await backend.load();
223
+ } catch (error) {
224
+ throw sanitizedBackendError(error, operation);
225
+ }
226
+ if (serialized === null) return null;
227
+ if (typeof serialized !== "string") {
228
+ throw secureCredentialBackendError(STORE_BACKEND, operation, "invalid_response");
229
+ }
230
+ return serialized;
231
+ }
232
+
233
+ async function saveToBackend(backend, serialized) {
234
+ try {
235
+ const result = await backend.save(serialized);
236
+ if (result !== undefined) {
237
+ throw secureCredentialBackendError(STORE_BACKEND, "save", "invalid_response");
238
+ }
239
+ } catch (error) {
240
+ throw sanitizedBackendError(error, "save");
241
+ }
242
+ }
243
+
244
+ async function saveVerifiedRecord(backend, record, serialized = undefined) {
245
+ const expected = serialized ?? serializeTrialCredentialRecord(record);
246
+ await saveToBackend(backend, expected);
247
+ const stored = await loadFromBackend(backend, "verify");
248
+ if (stored === null || serializeTrialCredentialRecord(stored) !== expected) {
249
+ throw secureCredentialBackendError(STORE_BACKEND, "save", "storage_failed");
250
+ }
251
+ return stored;
252
+ }
253
+
254
+ function sanitizedBackendError(error, operation) {
255
+ const reason = error instanceof SecureCredentialBackendError
256
+ ? error.reason
257
+ : "command_failed";
258
+ return secureCredentialBackendError(STORE_BACKEND, operation, reason);
259
+ }
260
+
261
+ function isProvisioningSeed(record) {
262
+ return record.state === "provisioning" && record.account_id === null;
263
+ }
264
+
265
+ function nonEmptyString(value) {
266
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
267
+ }
268
+
269
+ function isRecord(value) {
270
+ return typeof value === "object" && value !== null && !Array.isArray(value);
271
+ }
@@ -0,0 +1,440 @@
1
+ import { randomBytes as nodeRandomBytes } from "node:crypto";
2
+ import * as nodeFs from "node:fs/promises";
3
+ import { basename, dirname, isAbsolute, join, normalize, win32 } from "node:path";
4
+ import {
5
+ DEFAULT_SECURE_COMMAND_OUTPUT_BYTES,
6
+ DEFAULT_SECURE_COMMAND_TIMEOUT_MS,
7
+ SecureCredentialBackendError,
8
+ decodeSecureCredential,
9
+ encodeSecureCredential,
10
+ environmentValue,
11
+ executeSecureCommand,
12
+ minimalEnvironment,
13
+ runSecureCommand,
14
+ secureCredentialBackendError,
15
+ validateSecureCredentialNamespace,
16
+ wipeSecureCommandResult,
17
+ } from "./secure-command.mjs";
18
+
19
+ const BACKEND = "windows-dpapi";
20
+ const CIPHERTEXT_LIMIT_BYTES = DEFAULT_SECURE_COMMAND_OUTPUT_BYTES;
21
+ const WINDOWS_ENVIRONMENT_NAMES = Object.freeze([
22
+ "COMSPEC",
23
+ "TEMP",
24
+ "TMP",
25
+ "USERPROFILE",
26
+ ]);
27
+ const CREDENTIAL_FILE_NAME = "trial-credentials.v1.dpapi";
28
+ const DPAPI_ENTROPY_PREFIX = "MemoraX.Code.TrialCredentials.DPAPI.v1";
29
+ const DPAPI_ENTROPY_PLACEHOLDER = "__MEMORAX_DPAPI_ENTROPY__";
30
+ const CREDENTIAL_DIRECTORY_NAMES = Object.freeze([
31
+ "MemoraX Code",
32
+ "credentials",
33
+ ]);
34
+
35
+ const PROTECT_SCRIPT = String.raw`
36
+ $ErrorActionPreference = 'Stop'
37
+ $exitCode = 0
38
+ $sourceBytes = $null
39
+ $protectedBytes = $null
40
+ $entropyBytes = $null
41
+ $memoryBytes = $null
42
+ $memory = $null
43
+ $inputStream = $null
44
+ try {
45
+ Add-Type -AssemblyName System.Security
46
+ $memory = New-Object System.IO.MemoryStream
47
+ $inputStream = [Console]::OpenStandardInput()
48
+ $inputStream.CopyTo($memory)
49
+ [byte[]]$sourceBytes = $memory.ToArray()
50
+ [byte[]]$entropyBytes = [Text.Encoding]::UTF8.GetBytes('${DPAPI_ENTROPY_PLACEHOLDER}')
51
+ [byte[]]$protectedBytes = [Security.Cryptography.ProtectedData]::Protect(
52
+ $sourceBytes,
53
+ $entropyBytes,
54
+ [Security.Cryptography.DataProtectionScope]::CurrentUser
55
+ )
56
+ $output = [Console]::OpenStandardOutput()
57
+ $output.Write($protectedBytes, 0, $protectedBytes.Length)
58
+ $output.Flush()
59
+ } catch {
60
+ [Console]::Error.Write('dpapi_protect_failed')
61
+ $exitCode = 20
62
+ } finally {
63
+ if ($null -ne $sourceBytes) { [Array]::Clear($sourceBytes, 0, $sourceBytes.Length) }
64
+ if ($null -ne $protectedBytes) { [Array]::Clear($protectedBytes, 0, $protectedBytes.Length) }
65
+ if ($null -ne $entropyBytes) { [Array]::Clear($entropyBytes, 0, $entropyBytes.Length) }
66
+ if ($null -ne $memory) {
67
+ try {
68
+ [byte[]]$memoryBytes = $memory.GetBuffer()
69
+ [Array]::Clear($memoryBytes, 0, $memoryBytes.Length)
70
+ } finally {
71
+ $memory.Dispose()
72
+ }
73
+ }
74
+ }
75
+ exit $exitCode
76
+ `;
77
+
78
+ const UNPROTECT_SCRIPT = String.raw`
79
+ $ErrorActionPreference = 'Stop'
80
+ $exitCode = 0
81
+ $sourceBytes = $null
82
+ $plainBytes = $null
83
+ $entropyBytes = $null
84
+ $memoryBytes = $null
85
+ $memory = $null
86
+ $inputStream = $null
87
+ try {
88
+ Add-Type -AssemblyName System.Security
89
+ $memory = New-Object System.IO.MemoryStream
90
+ $inputStream = [Console]::OpenStandardInput()
91
+ $inputStream.CopyTo($memory)
92
+ [byte[]]$sourceBytes = $memory.ToArray()
93
+ [byte[]]$entropyBytes = [Text.Encoding]::UTF8.GetBytes('${DPAPI_ENTROPY_PLACEHOLDER}')
94
+ [byte[]]$plainBytes = [Security.Cryptography.ProtectedData]::Unprotect(
95
+ $sourceBytes,
96
+ $entropyBytes,
97
+ [Security.Cryptography.DataProtectionScope]::CurrentUser
98
+ )
99
+ $output = [Console]::OpenStandardOutput()
100
+ $output.Write($plainBytes, 0, $plainBytes.Length)
101
+ $output.Flush()
102
+ } catch {
103
+ [Console]::Error.Write('dpapi_unprotect_failed')
104
+ $exitCode = 21
105
+ } finally {
106
+ if ($null -ne $sourceBytes) { [Array]::Clear($sourceBytes, 0, $sourceBytes.Length) }
107
+ if ($null -ne $plainBytes) { [Array]::Clear($plainBytes, 0, $plainBytes.Length) }
108
+ if ($null -ne $entropyBytes) { [Array]::Clear($entropyBytes, 0, $entropyBytes.Length) }
109
+ if ($null -ne $memory) {
110
+ try {
111
+ [byte[]]$memoryBytes = $memory.GetBuffer()
112
+ [Array]::Clear($memoryBytes, 0, $memoryBytes.Length)
113
+ } finally {
114
+ $memory.Dispose()
115
+ }
116
+ }
117
+ }
118
+ exit $exitCode
119
+ `;
120
+
121
+ export function createWindowsDpapiBackend(options) {
122
+ const namespace = validateSecureCredentialNamespace(options?.namespace, BACKEND);
123
+ const sourceEnvironment = options?.environment ?? process.env;
124
+ const systemRoot = options?.systemRoot
125
+ ?? environmentValue(sourceEnvironment, "SystemRoot")
126
+ ?? environmentValue(sourceEnvironment, "WINDIR");
127
+ if (typeof systemRoot !== "string" || !win32.isAbsolute(systemRoot)) {
128
+ throw secureCredentialBackendError(BACKEND, "initialize", "backend_unavailable");
129
+ }
130
+ const localAppData = options?.localAppData
131
+ ?? environmentValue(sourceEnvironment, "LOCALAPPDATA");
132
+ if (typeof localAppData !== "string" || !isAbsolute(localAppData)) {
133
+ throw secureCredentialBackendError(BACKEND, "initialize", "unsafe_path");
134
+ }
135
+ const storageRoot = normalizeLocalAppData(localAppData);
136
+
137
+ const runner = options?.runner ?? runSecureCommand;
138
+ const fileSystem = options?.fileSystem ?? nodeFs;
139
+ const randomBytes = options?.randomBytes ?? nodeRandomBytes;
140
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_SECURE_COMMAND_TIMEOUT_MS;
141
+ const maxOutputBytes = options?.maxOutputBytes ?? DEFAULT_SECURE_COMMAND_OUTPUT_BYTES;
142
+ const powerShell = win32.join(
143
+ systemRoot,
144
+ "System32",
145
+ "WindowsPowerShell",
146
+ "v1.0",
147
+ "powershell.exe",
148
+ );
149
+ const environment = minimalEnvironment(
150
+ sourceEnvironment,
151
+ WINDOWS_ENVIRONMENT_NAMES,
152
+ { SystemRoot: systemRoot, WINDIR: systemRoot },
153
+ );
154
+ const credentialDirectoryNames = [...CREDENTIAL_DIRECTORY_NAMES, namespace];
155
+ const credentialDirectory = join(storageRoot, ...credentialDirectoryNames);
156
+ const credentialPath = join(credentialDirectory, CREDENTIAL_FILE_NAME);
157
+ const protectScript = bindDpapiEntropy(PROTECT_SCRIPT, namespace);
158
+ const unprotectScript = bindDpapiEntropy(UNPROTECT_SCRIPT, namespace);
159
+
160
+ async function invoke(operation, script, input) {
161
+ return executeSecureCommand(runner, {
162
+ backend: BACKEND,
163
+ operation,
164
+ command: powerShell,
165
+ args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
166
+ input,
167
+ env: environment,
168
+ timeoutMs,
169
+ maxOutputBytes,
170
+ });
171
+ }
172
+
173
+ return Object.freeze({
174
+ async load() {
175
+ const ciphertext = await readCiphertext(
176
+ fileSystem,
177
+ storageRoot,
178
+ credentialDirectoryNames,
179
+ credentialPath,
180
+ );
181
+ if (ciphertext === null) return null;
182
+ try {
183
+ const result = await invoke("load", unprotectScript, ciphertext);
184
+ try {
185
+ if (result.status !== 0 || result.signal !== null) {
186
+ throw secureCredentialBackendError(BACKEND, "load", "command_failed");
187
+ }
188
+ return decodeSecureCredential(result.stdout, BACKEND, "load");
189
+ } finally {
190
+ wipeSecureCommandResult(result);
191
+ }
192
+ } finally {
193
+ ciphertext.fill(0);
194
+ }
195
+ },
196
+
197
+ async save(serialized) {
198
+ const plaintext = encodeSecureCredential(serialized, BACKEND, "save");
199
+ let ciphertext;
200
+ try {
201
+ const result = await invoke("save", protectScript, plaintext);
202
+ try {
203
+ if (result.status !== 0
204
+ || result.signal !== null
205
+ || result.stdout.length === 0
206
+ || result.stdout.includes(plaintext)) {
207
+ throw secureCredentialBackendError(BACKEND, "save", "command_failed");
208
+ }
209
+ if (result.stdout.length > CIPHERTEXT_LIMIT_BYTES) {
210
+ throw secureCredentialBackendError(BACKEND, "save", "output_limit");
211
+ }
212
+ ciphertext = Buffer.from(result.stdout);
213
+ } finally {
214
+ wipeSecureCommandResult(result);
215
+ }
216
+ } finally {
217
+ plaintext.fill(0);
218
+ }
219
+ try {
220
+ await writeCiphertextAtomically(
221
+ fileSystem,
222
+ storageRoot,
223
+ credentialDirectoryNames,
224
+ credentialPath,
225
+ ciphertext,
226
+ randomBytes,
227
+ );
228
+ } finally {
229
+ ciphertext.fill(0);
230
+ }
231
+ },
232
+
233
+ async delete() {
234
+ const directoryPresent = await requireSafeCredentialDirectory(
235
+ fileSystem,
236
+ storageRoot,
237
+ credentialDirectoryNames,
238
+ "delete",
239
+ false,
240
+ );
241
+ if (!directoryPresent) return false;
242
+ const present = await requireRegularFileOrMissing(fileSystem, credentialPath, "delete");
243
+ if (!present) return false;
244
+ try {
245
+ await fileSystem.unlink(credentialPath);
246
+ return true;
247
+ } catch (error) {
248
+ throw sanitizeStorageFailure(error, "delete");
249
+ }
250
+ },
251
+ });
252
+ }
253
+
254
+ async function readCiphertext(
255
+ fileSystem,
256
+ storageRoot,
257
+ credentialDirectoryNames,
258
+ path,
259
+ ) {
260
+ const directoryPresent = await requireSafeCredentialDirectory(
261
+ fileSystem,
262
+ storageRoot,
263
+ credentialDirectoryNames,
264
+ "load",
265
+ false,
266
+ );
267
+ if (!directoryPresent) return null;
268
+ const present = await requireRegularFileOrMissing(fileSystem, path, "load");
269
+ if (!present) return null;
270
+ let ciphertext;
271
+ try {
272
+ ciphertext = await fileSystem.readFile(path);
273
+ } catch (error) {
274
+ throw sanitizeStorageFailure(error, "load");
275
+ }
276
+ const bytes = Buffer.from(ciphertext);
277
+ if (bytes.length === 0 || bytes.length > CIPHERTEXT_LIMIT_BYTES) {
278
+ bytes.fill(0);
279
+ throw secureCredentialBackendError(BACKEND, "load", "invalid_response");
280
+ }
281
+ return bytes;
282
+ }
283
+
284
+ async function writeCiphertextAtomically(
285
+ fileSystem,
286
+ storageRoot,
287
+ credentialDirectoryNames,
288
+ path,
289
+ ciphertext,
290
+ randomBytes,
291
+ ) {
292
+ const directory = dirname(path);
293
+ await requireSafeCredentialDirectory(
294
+ fileSystem,
295
+ storageRoot,
296
+ credentialDirectoryNames,
297
+ "save",
298
+ true,
299
+ );
300
+ await requireRegularFileOrMissing(fileSystem, path, "save");
301
+
302
+ let suffix;
303
+ try {
304
+ suffix = Buffer.from(randomBytes(12)).toString("hex");
305
+ } catch (error) {
306
+ throw sanitizeStorageFailure(error, "save");
307
+ }
308
+ if (!/^[0-9a-f]{24}$/.test(suffix)) {
309
+ throw secureCredentialBackendError(BACKEND, "save", "storage_failed");
310
+ }
311
+ const temporaryPath = join(directory, `.${basename(path)}.${process.pid}.${suffix}.tmp`);
312
+ let handle;
313
+ try {
314
+ handle = await fileSystem.open(temporaryPath, "wx", 0o600);
315
+ await handle.writeFile(ciphertext);
316
+ await handle.sync();
317
+ await handle.close();
318
+ handle = undefined;
319
+ const directoryStillPresent = await requireSafeCredentialDirectory(
320
+ fileSystem,
321
+ storageRoot,
322
+ credentialDirectoryNames,
323
+ "save",
324
+ false,
325
+ );
326
+ if (!directoryStillPresent) {
327
+ throw secureCredentialBackendError(BACKEND, "save", "storage_failed");
328
+ }
329
+ await requireRegularFileOrMissing(fileSystem, path, "save");
330
+ await fileSystem.rename(temporaryPath, path);
331
+ } catch (error) {
332
+ if (handle !== undefined) {
333
+ try {
334
+ await handle.close();
335
+ } catch {
336
+ // Preserve the original, redacted storage failure.
337
+ }
338
+ }
339
+ try {
340
+ await fileSystem.unlink(temporaryPath);
341
+ } catch {
342
+ // Best-effort cleanup; the original failure remains authoritative.
343
+ }
344
+ throw sanitizeStorageFailure(error, "save");
345
+ }
346
+ }
347
+
348
+ async function requireSafeCredentialDirectory(
349
+ fileSystem,
350
+ storageRoot,
351
+ childNames,
352
+ operation,
353
+ create,
354
+ ) {
355
+ await requireRealDirectory(fileSystem, storageRoot, operation, false);
356
+ let currentPath = storageRoot;
357
+ for (const name of childNames) {
358
+ currentPath = join(currentPath, name);
359
+ const present = await requireRealDirectory(
360
+ fileSystem,
361
+ currentPath,
362
+ operation,
363
+ true,
364
+ );
365
+ if (present) continue;
366
+ if (!create) return false;
367
+ try {
368
+ await fileSystem.mkdir(currentPath, { mode: 0o700 });
369
+ } catch (error) {
370
+ if (!hasErrorCode(error, "EEXIST")) {
371
+ throw sanitizeStorageFailure(error, operation);
372
+ }
373
+ }
374
+ await requireRealDirectory(fileSystem, currentPath, operation, false);
375
+ }
376
+ return true;
377
+ }
378
+
379
+ async function requireRealDirectory(fileSystem, path, operation, allowMissing) {
380
+ let status;
381
+ try {
382
+ status = await fileSystem.lstat(path);
383
+ } catch (error) {
384
+ if (allowMissing && isMissing(error)) return false;
385
+ throw sanitizeStorageFailure(error, operation);
386
+ }
387
+ if (status.isSymbolicLink() || !status.isDirectory()) {
388
+ throw secureCredentialBackendError(BACKEND, operation, "unsafe_path");
389
+ }
390
+ return true;
391
+ }
392
+
393
+ async function requireRegularFileOrMissing(fileSystem, path, operation) {
394
+ let status;
395
+ try {
396
+ status = await fileSystem.lstat(path);
397
+ } catch (error) {
398
+ if (isMissing(error)) return false;
399
+ throw sanitizeStorageFailure(error, operation);
400
+ }
401
+ if (status.isSymbolicLink() || !status.isFile()) {
402
+ throw secureCredentialBackendError(BACKEND, operation, "unsafe_path");
403
+ }
404
+ return true;
405
+ }
406
+
407
+ function sanitizeStorageFailure(error, operation) {
408
+ if (error instanceof SecureCredentialBackendError) return error;
409
+ return secureCredentialBackendError(BACKEND, operation, "storage_failed");
410
+ }
411
+
412
+ function bindDpapiEntropy(script, namespace) {
413
+ return script.replace(
414
+ DPAPI_ENTROPY_PLACEHOLDER,
415
+ `${DPAPI_ENTROPY_PREFIX}:${namespace}`,
416
+ );
417
+ }
418
+
419
+ function normalizeLocalAppData(localAppData) {
420
+ const normalized = normalize(localAppData);
421
+ if (process.platform === "win32") {
422
+ const root = win32.parse(normalized).root;
423
+ if (!/^[A-Za-z]:\\$/.test(root)
424
+ || normalized.toLowerCase() === root.toLowerCase()) {
425
+ throw secureCredentialBackendError(BACKEND, "initialize", "unsafe_path");
426
+ }
427
+ }
428
+ return normalized;
429
+ }
430
+
431
+ function isMissing(error) {
432
+ return hasErrorCode(error, "ENOENT");
433
+ }
434
+
435
+ function hasErrorCode(error, code) {
436
+ return typeof error === "object"
437
+ && error !== null
438
+ && "code" in error
439
+ && error.code === code;
440
+ }