@blamejs/core 0.8.42 → 0.8.49

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -0,0 +1,865 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.keychain
4
+ * @nav Crypto
5
+ * @title Keychain
6
+ *
7
+ * @intro
8
+ * OS keychain abstraction with encrypted-file fallback — stores /
9
+ * retrieves / removes a `(service, account) -> password` binding via
10
+ * the host operating system's native credential store. Operators
11
+ * reach for this from CLI bootstraps that need to materialize a
12
+ * database password, outbound webhook secret, SMTP relay credential,
13
+ * etc., without baking the value into a config file or env var.
14
+ *
15
+ * Backend dispatch:
16
+ * - macOS -> `/usr/bin/security` (add-/find-/delete-generic-password)
17
+ * - Linux -> `secret-tool` from libsecret; password on stdin so
18
+ * it never reaches /proc/<pid>/cmdline
19
+ * - Windows -> PowerShell + the CredentialManager module; password
20
+ * on stdin via [Console]::In.ReadToEnd(). Falls through
21
+ * to the file backend when CredentialManager is absent
22
+ * - File -> XChaCha20-Poly1305-sealed JSON whose KEK is derived
23
+ * via Argon2id from `opts.passphrase`. Wrap format is
24
+ * shared with `b.vault.wrap` (magic 0xE2). File mode
25
+ * 0o600, atomic via `b.atomicFile.write`.
26
+ *
27
+ * Process-list-safety: every native-tool invocation passes the
28
+ * password on stdin. macOS `security` is invoked with `-w -` (the
29
+ * documented stdin sentinel); secret-tool always reads from stdin;
30
+ * PowerShell scripts use `[Console]::In.ReadToEnd()`. The plaintext
31
+ * never crosses argv on any backend.
32
+ *
33
+ * Validation tier: config-time / entry-point. Bad opts throw
34
+ * `KeychainError` synchronously; native-tool failures surface as
35
+ * `KeychainError` with the tool's stderr included.
36
+ *
37
+ * Audit: every call emits one of `keychain.stored` / `keychain.retrieved`
38
+ * / `keychain.removed` (or the `.failed` sibling). Audit metadata
39
+ * records service / account / backend / outcome. The password value
40
+ * is never audited.
41
+ *
42
+ * @card
43
+ * OS keychain abstraction with encrypted-file fallback — stores / retrieves / removes a `(service, account) -> password` binding via the host operating system's native credential store.
44
+ */
45
+
46
+ var fs = require("fs");
47
+ var path = require("path");
48
+
49
+ var atomicFile = require("./atomic-file");
50
+ var C = require("./constants");
51
+ var lazyRequire = require("./lazy-require");
52
+ var processSpawn = require("./process-spawn");
53
+ var safeBuffer = require("./safe-buffer");
54
+ var safeEnv = require("./parsers/safe-env");
55
+ var safeJson = require("./safe-json");
56
+ var validateOpts = require("./validate-opts");
57
+ var vaultWrap = require("./vault/wrap");
58
+ var { FrameworkError, KeychainError } = require("./framework-error");
59
+
60
+ var audit = lazyRequire(function () { return require("./audit"); });
61
+
62
+ // ---- Backend detection -----------------------------------------------------
63
+
64
+ // Cached per-process so multiple calls don't re-stat /usr/bin/security
65
+ // on every invocation. Detection is best-effort — `null` means "fall
66
+ // through to the next backend / file fallback".
67
+ var _cachedBackend = null;
68
+
69
+ function _detectBackend() {
70
+ if (_cachedBackend !== null) return _cachedBackend;
71
+ var p = process.platform;
72
+ if (p === "darwin") {
73
+ if (_existsExecutable("/usr/bin/security")) {
74
+ _cachedBackend = "macos-security";
75
+ return _cachedBackend;
76
+ }
77
+ } else if (p === "linux") {
78
+ if (_resolveOnPath("secret-tool")) {
79
+ _cachedBackend = "linux-secret-tool";
80
+ return _cachedBackend;
81
+ }
82
+ } else if (p === "win32") {
83
+ if (_resolveOnPath("powershell.exe") || _resolveOnPath("pwsh.exe")) {
84
+ _cachedBackend = "windows-credential";
85
+ return _cachedBackend;
86
+ }
87
+ }
88
+ _cachedBackend = null;
89
+ return null;
90
+ }
91
+
92
+ function _existsExecutable(filepath) {
93
+ try {
94
+ var st = fs.statSync(filepath);
95
+ return st.isFile();
96
+ } catch (_e) { return false; }
97
+ }
98
+
99
+ // Allowlist of bin names this module is permitted to resolve on PATH.
100
+ // Frozen so a future caller can't smuggle an attacker-controlled name
101
+ // through _resolveOnPath — the lookup is gated by hardcoded membership
102
+ // in this set, not by any operator-supplied opts.
103
+ var _PATH_RESOLVE_ALLOWLIST = Object.freeze({
104
+ "secret-tool": true,
105
+ "powershell.exe": true,
106
+ "pwsh.exe": true,
107
+ });
108
+
109
+ function _resolveOnPath(binName) {
110
+ if (typeof binName !== "string" || _PATH_RESOLVE_ALLOWLIST[binName] !== true) {
111
+ return null;
112
+ }
113
+ // Reject any bin name with a path separator — defense in depth on top
114
+ // of the allowlist; a future contributor adding to the allowlist
115
+ // can't accidentally land a value with a "/" or "\" in it.
116
+ if (binName.indexOf("/") !== -1 || binName.indexOf("\\") !== -1) return null;
117
+ // Windows env vars are case-insensitive; Node populates both PATH and Path.
118
+ // safeEnv.readVar gates each by name with the standard size cap.
119
+ var pathEnv = safeEnv.readVar("PATH", { default: "" }) ||
120
+ safeEnv.readVar("Path", { default: "" }) ||
121
+ "";
122
+ var sep = process.platform === "win32" ? ";" : ":";
123
+ var parts = pathEnv.split(sep);
124
+ for (var i = 0; i < parts.length; i += 1) {
125
+ var dir = parts[i];
126
+ if (typeof dir !== "string" || dir.length === 0) continue;
127
+ var candidate = path.join(dir, binName);
128
+ if (_existsExecutable(candidate)) return candidate;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ // ---- Opts validation -------------------------------------------------------
134
+
135
+ function _validateCommonOpts(opts, primitive) {
136
+ if (opts == null || typeof opts !== "object") {
137
+ throw new KeychainError("keychain/bad-opts",
138
+ primitive + ": opts must be an object");
139
+ }
140
+ validateOpts(opts, [
141
+ "service", "account", "password", "fallbackFile", "passphrase",
142
+ "preferFile", "audit",
143
+ ], primitive);
144
+ validateOpts.requireNonEmptyString(opts.service, "service",
145
+ KeychainError, "keychain/bad-service");
146
+ validateOpts.requireNonEmptyString(opts.account, "account",
147
+ KeychainError, "keychain/bad-account");
148
+ // Service / account names cross argv on every native backend. Refuse
149
+ // newline / null bytes universally — they enable command injection on
150
+ // secret-tool's attribute-list parser and embed-an-extra-key on
151
+ // PowerShell's -Target string.
152
+ // eslint-disable-next-line no-control-regex
153
+ if (/[\u0000\r\n]/.test(opts.service) || /[\u0000\r\n]/.test(opts.account)) {
154
+ throw new KeychainError("keychain/bad-identifier",
155
+ primitive + ": service/account must not contain null or newline bytes");
156
+ }
157
+ }
158
+
159
+ function _validateFallbackFile(filepath, primitive) {
160
+ validateOpts.requireNonEmptyString(filepath, "fallbackFile",
161
+ KeychainError, "keychain/bad-fallback-file");
162
+ if (!path.isAbsolute(filepath)) {
163
+ throw new KeychainError("keychain/relative-fallback-file",
164
+ primitive + ": fallbackFile must be an absolute path; got " + filepath);
165
+ }
166
+ }
167
+
168
+ // ---- File-fallback I/O -----------------------------------------------------
169
+ //
170
+ // File format: vault.wrap-sealed buffer whose plaintext is a canonical
171
+ // JSON document of the shape
172
+ //
173
+ // { version: 1, entries: { "<service>\u0000<account>": "<password>" } }
174
+ //
175
+ // One file holds every binding for the operator's process. Atomic
176
+ // rename on every write (atomicFile.write) so a crash never leaves a
177
+ // half-written ciphertext at `fallbackFile`.
178
+
179
+ var FILE_FORMAT_VERSION = 1;
180
+ var FILE_KEY_SEPARATOR = "\u0000";
181
+
182
+ function _bindingKey(service, account) {
183
+ return service + FILE_KEY_SEPARATOR + account;
184
+ }
185
+
186
+ async function _readFile(fallbackFile, passphrase) {
187
+ if (!atomicFile.exists(fallbackFile)) {
188
+ return { version: FILE_FORMAT_VERSION, entries: {} };
189
+ }
190
+ validateOpts.requireNonEmptyString(passphrase, "passphrase",
191
+ KeychainError, "keychain/file-passphrase-required");
192
+ var sealed = await atomicFile.read(fallbackFile, {
193
+ maxBytes: C.BYTES.mib(4),
194
+ });
195
+ if (!Buffer.isBuffer(sealed)) sealed = Buffer.from(sealed);
196
+ var pwBuf = Buffer.from(String(passphrase), "utf8");
197
+ var plaintext;
198
+ try {
199
+ plaintext = await vaultWrap.unwrap(sealed, pwBuf);
200
+ } catch (_e) {
201
+ throw new KeychainError("keychain/file-unseal-failed",
202
+ "fallback file passphrase rejected or file corrupted");
203
+ } finally {
204
+ safeBuffer.secureZero(pwBuf);
205
+ }
206
+ var doc;
207
+ try {
208
+ doc = safeJson.parse(plaintext.toString("utf8"));
209
+ } catch (_e) {
210
+ safeBuffer.secureZero(plaintext);
211
+ throw new KeychainError("keychain/file-bad-shape",
212
+ "fallback file payload is not valid JSON");
213
+ }
214
+ safeBuffer.secureZero(plaintext);
215
+ if (!doc || typeof doc !== "object" || doc.version !== FILE_FORMAT_VERSION ||
216
+ !doc.entries || typeof doc.entries !== "object") {
217
+ throw new KeychainError("keychain/file-bad-shape",
218
+ "fallback file is not a keychain document");
219
+ }
220
+ return doc;
221
+ }
222
+
223
+ async function _writeFile(fallbackFile, doc, passphrase) {
224
+ validateOpts.requireNonEmptyString(passphrase, "passphrase",
225
+ KeychainError, "keychain/file-passphrase-required");
226
+ var serialized = Buffer.from(safeJson.canonical(doc), "utf8");
227
+ var pwBuf = Buffer.from(String(passphrase), "utf8");
228
+ var sealed;
229
+ try {
230
+ sealed = await vaultWrap.wrap(serialized, pwBuf);
231
+ } finally {
232
+ safeBuffer.secureZero(pwBuf);
233
+ safeBuffer.secureZero(serialized);
234
+ }
235
+ // atomicFile.write enforces 0o600 by default and writes via
236
+ // temp + fsync + rename so a crash never leaves a partial file.
237
+ await atomicFile.write(fallbackFile, sealed, { fileMode: 0o600 });
238
+ }
239
+
240
+ // ---- Native-backend invocations -------------------------------------------
241
+
242
+ // Drain a stream into a Buffer, honoring an upper byte cap so a
243
+ // runaway tool can't OOM the framework.
244
+ function _drain(stream, capBytes) {
245
+ return new Promise(function (resolve, reject) {
246
+ if (!stream) { resolve(Buffer.alloc(0)); return; }
247
+ var collector = safeBuffer.boundedChunkCollector({
248
+ maxBytes: capBytes,
249
+ errorClass: KeychainError,
250
+ sizeCode: "keychain/native-output-too-large",
251
+ sizeMessage: "native tool output exceeded " + capBytes + " bytes",
252
+ });
253
+ stream.on("data", function (chunk) {
254
+ try {
255
+ collector.push(chunk);
256
+ } catch (e) {
257
+ try { stream.destroy(); } catch (_e) { /* destroy best-effort */ }
258
+ reject(e);
259
+ }
260
+ });
261
+ stream.on("end", function () { resolve(collector.result()); });
262
+ stream.on("error", function (e) { reject(e); });
263
+ });
264
+ }
265
+
266
+ function _runNative(command, args, opts) {
267
+ opts = opts || {};
268
+ var stdinBuf = opts.stdin == null ? null
269
+ : (Buffer.isBuffer(opts.stdin) ? opts.stdin : Buffer.from(String(opts.stdin), "utf8"));
270
+ return new Promise(function (resolve, reject) {
271
+ var child;
272
+ try {
273
+ child = processSpawn.spawn(command, args || [], {
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ });
276
+ } catch (e) { reject(e); return; }
277
+
278
+ var stdoutCap = opts.maxStdoutBytes || C.BYTES.mib(1);
279
+ var stderrCap = opts.maxStderrBytes || C.BYTES.kib(64);
280
+ var settled = false;
281
+ var outP = _drain(child.stdout, stdoutCap);
282
+ var errP = _drain(child.stderr, stderrCap);
283
+
284
+ child.on("error", function (e) {
285
+ if (settled) return;
286
+ settled = true;
287
+ if (stdinBuf) safeBuffer.secureZero(stdinBuf);
288
+ reject(e);
289
+ });
290
+
291
+ child.on("close", function (code, signal) {
292
+ Promise.all([outP, errP]).then(function (bufs) {
293
+ if (settled) return;
294
+ settled = true;
295
+ if (stdinBuf) safeBuffer.secureZero(stdinBuf);
296
+ resolve({
297
+ code: typeof code === "number" ? code : -1,
298
+ signal: signal || null,
299
+ stdout: bufs[0],
300
+ stderr: bufs[1],
301
+ });
302
+ }, function (e) {
303
+ if (settled) return;
304
+ settled = true;
305
+ if (stdinBuf) safeBuffer.secureZero(stdinBuf);
306
+ reject(e);
307
+ });
308
+ });
309
+
310
+ if (stdinBuf && child.stdin) {
311
+ try {
312
+ child.stdin.on("error", function (_e) { /* broken pipe is fine */ });
313
+ child.stdin.end(stdinBuf);
314
+ } catch (_e) { /* tool may have closed stdin already */ }
315
+ } else if (child.stdin) {
316
+ try { child.stdin.end(); } catch (_e) { /* close best-effort */ }
317
+ }
318
+ });
319
+ }
320
+
321
+ // ---- macOS: /usr/bin/security ---------------------------------------------
322
+ //
323
+ // add-generic-password supports `-w -` to read the password from stdin
324
+ // (man security(1) — "If pre-existing or read with -w -, the password
325
+ // is read from stdin"). The single dash is the documented sentinel.
326
+
327
+ async function _macStore(service, account, password) {
328
+ var r = await _runNative("/usr/bin/security", [
329
+ "add-generic-password",
330
+ "-s", service,
331
+ "-a", account,
332
+ "-w", "-", // read password from stdin
333
+ "-U", // update if exists
334
+ ], { stdin: password });
335
+ if (r.code !== 0) {
336
+ throw new KeychainError("keychain/macos-store-failed",
337
+ "security add-generic-password exited " + r.code + ": " +
338
+ r.stderr.toString("utf8").trim());
339
+ }
340
+ }
341
+
342
+ async function _macRetrieve(service, account) {
343
+ var r = await _runNative("/usr/bin/security", [
344
+ "find-generic-password",
345
+ "-s", service,
346
+ "-a", account,
347
+ "-w", // print password on stdout
348
+ ]);
349
+ if (r.code === 44) { // SecKeychainSearchCopyNext: not found
350
+ return null;
351
+ }
352
+ if (r.code !== 0) {
353
+ throw new KeychainError("keychain/macos-retrieve-failed",
354
+ "security find-generic-password exited " + r.code + ": " +
355
+ r.stderr.toString("utf8").trim());
356
+ }
357
+ // `security -w` prints the password followed by a newline.
358
+ var raw = r.stdout.toString("utf8");
359
+ if (raw.length > 0 && raw[raw.length - 1] === "\n") raw = raw.slice(0, -1);
360
+ return raw;
361
+ }
362
+
363
+ async function _macRemove(service, account) {
364
+ var r = await _runNative("/usr/bin/security", [
365
+ "delete-generic-password",
366
+ "-s", service,
367
+ "-a", account,
368
+ ]);
369
+ if (r.code === 44) return false;
370
+ if (r.code !== 0) {
371
+ throw new KeychainError("keychain/macos-remove-failed",
372
+ "security delete-generic-password exited " + r.code + ": " +
373
+ r.stderr.toString("utf8").trim());
374
+ }
375
+ return true;
376
+ }
377
+
378
+ // ---- Linux: secret-tool ----------------------------------------------------
379
+ //
380
+ // `secret-tool store` reads the password from stdin only — there is no
381
+ // CLI flag for the value (man secret-tool — "Will prompt for the secret
382
+ // or read it from standard input if it isn't a TTY"). Stdin path is
383
+ // process-list-safe by construction.
384
+
385
+ async function _linuxStore(service, account, password) {
386
+ var r = await _runNative("secret-tool", [
387
+ "store",
388
+ "--label", service,
389
+ "service", service,
390
+ "account", account,
391
+ ], { stdin: password });
392
+ if (r.code !== 0) {
393
+ throw new KeychainError("keychain/linux-store-failed",
394
+ "secret-tool store exited " + r.code + ": " +
395
+ r.stderr.toString("utf8").trim());
396
+ }
397
+ }
398
+
399
+ async function _linuxRetrieve(service, account) {
400
+ var r = await _runNative("secret-tool", [
401
+ "lookup",
402
+ "service", service,
403
+ "account", account,
404
+ ]);
405
+ if (r.code !== 0) {
406
+ // secret-tool exits 1 when the attribute set has no match. Surface
407
+ // null instead of throwing — operators expect "not found" to return
408
+ // null rather than an error.
409
+ if (r.stderr.toString("utf8").indexOf("No matching") !== -1 || r.stdout.length === 0) {
410
+ return null;
411
+ }
412
+ throw new KeychainError("keychain/linux-retrieve-failed",
413
+ "secret-tool lookup exited " + r.code + ": " +
414
+ r.stderr.toString("utf8").trim());
415
+ }
416
+ var raw = r.stdout.toString("utf8");
417
+ // secret-tool does NOT append a newline (man secret-tool); guard
418
+ // anyway in case a future libsecret release changes that.
419
+ if (raw.length > 0 && raw[raw.length - 1] === "\n") raw = raw.slice(0, -1);
420
+ if (raw.length === 0) return null;
421
+ return raw;
422
+ }
423
+
424
+ async function _linuxRemove(service, account) {
425
+ var r = await _runNative("secret-tool", [
426
+ "clear",
427
+ "service", service,
428
+ "account", account,
429
+ ]);
430
+ if (r.code !== 0) {
431
+ throw new KeychainError("keychain/linux-remove-failed",
432
+ "secret-tool clear exited " + r.code + ": " +
433
+ r.stderr.toString("utf8").trim());
434
+ }
435
+ return true;
436
+ }
437
+
438
+ // ---- Windows: PowerShell + CredentialManager module -----------------------
439
+ //
440
+ // CredentialManager (PSGallery) exposes Get-/New-/Remove-StoredCredential.
441
+ // We pipe the password to PowerShell on stdin via $cred = [Console]::In.
442
+ // ReadLine() so the plaintext never hits argv. When CredentialManager is
443
+ // not installed (`Get-Module -ListAvailable CredentialManager` empty),
444
+ // the calling host gets a not-supported error and the keychain falls
445
+ // through to the file fallback.
446
+ //
447
+ // The script reads: command (one of "store" / "retrieve" / "remove"),
448
+ // service, account from argv; password (store only) from stdin.
449
+
450
+ var _PS_SCRIPT_HEAD = "" +
451
+ "$ErrorActionPreference = 'Stop';" +
452
+ "if (-not (Get-Module -ListAvailable -Name CredentialManager)) {" +
453
+ "Write-Error 'CredentialManager module not installed';" +
454
+ "exit 2;" +
455
+ "}" +
456
+ "Import-Module CredentialManager;";
457
+
458
+ function _psQuote(value) {
459
+ // PowerShell single-quoted strings escape ' as ''.
460
+ return "'" + String(value).replace(/'/g, "''") + "'";
461
+ }
462
+
463
+ function _psStoreScript(service, account) {
464
+ var target = service + ":" + account;
465
+ return _PS_SCRIPT_HEAD +
466
+ "$pw = [Console]::In.ReadToEnd();" +
467
+ "if ($pw.EndsWith([char]10)) { $pw = $pw.Substring(0, $pw.Length - 1); }" +
468
+ "if ($pw.EndsWith([char]13)) { $pw = $pw.Substring(0, $pw.Length - 1); }" +
469
+ "$secure = ConvertTo-SecureString -String $pw -AsPlainText -Force;" +
470
+ "New-StoredCredential -Target " + _psQuote(target) +
471
+ " -UserName " + _psQuote(account) +
472
+ " -SecurePassword $secure -Persist LocalMachine | Out-Null;" +
473
+ "Write-Output 'OK';";
474
+ }
475
+
476
+ function _psRetrieveScript(service, account) {
477
+ var target = service + ":" + account;
478
+ return _PS_SCRIPT_HEAD +
479
+ "$cred = Get-StoredCredential -Target " + _psQuote(target) + ";" +
480
+ "if ($null -eq $cred) { exit 44; }" +
481
+ "$ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password);" +
482
+ "try {" +
483
+ "$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($ptr);" +
484
+ "[Console]::Out.Write($plain);" +
485
+ "} finally {" +
486
+ "[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr);" +
487
+ "}";
488
+ }
489
+
490
+ function _psRemoveScript(service, account) {
491
+ var target = service + ":" + account;
492
+ return _PS_SCRIPT_HEAD +
493
+ "try {" +
494
+ "Remove-StoredCredential -Target " + _psQuote(target) + ";" +
495
+ "Write-Output 'OK';" +
496
+ "} catch {" +
497
+ "if ($_.Exception.Message -match 'not be found') { exit 44; } else { throw; }" +
498
+ "}";
499
+ }
500
+
501
+ function _psResolve() {
502
+ var p = _resolveOnPath("pwsh.exe") || _resolveOnPath("powershell.exe");
503
+ if (!p) {
504
+ throw new KeychainError("keychain/windows-no-powershell",
505
+ "PowerShell executable not found on PATH");
506
+ }
507
+ return p;
508
+ }
509
+
510
+ async function _windowsStore(service, account, password) {
511
+ var r = await _runNative(_psResolve(), [
512
+ "-NoProfile", "-NonInteractive", "-Command", _psStoreScript(service, account),
513
+ ], { stdin: password });
514
+ if (r.code === 2) {
515
+ var e = new KeychainError("keychain/windows-not-supported",
516
+ "CredentialManager PowerShell module not installed");
517
+ e.fallback = true;
518
+ throw e;
519
+ }
520
+ if (r.code !== 0) {
521
+ throw new KeychainError("keychain/windows-store-failed",
522
+ "PowerShell New-StoredCredential exited " + r.code + ": " +
523
+ r.stderr.toString("utf8").trim());
524
+ }
525
+ }
526
+
527
+ async function _windowsRetrieve(service, account) {
528
+ var r = await _runNative(_psResolve(), [
529
+ "-NoProfile", "-NonInteractive", "-Command", _psRetrieveScript(service, account),
530
+ ]);
531
+ if (r.code === 2) {
532
+ var e = new KeychainError("keychain/windows-not-supported",
533
+ "CredentialManager PowerShell module not installed");
534
+ e.fallback = true;
535
+ throw e;
536
+ }
537
+ if (r.code === 44) return null;
538
+ if (r.code !== 0) {
539
+ throw new KeychainError("keychain/windows-retrieve-failed",
540
+ "PowerShell Get-StoredCredential exited " + r.code + ": " +
541
+ r.stderr.toString("utf8").trim());
542
+ }
543
+ return r.stdout.toString("utf8");
544
+ }
545
+
546
+ async function _windowsRemove(service, account) {
547
+ var r = await _runNative(_psResolve(), [
548
+ "-NoProfile", "-NonInteractive", "-Command", _psRemoveScript(service, account),
549
+ ]);
550
+ if (r.code === 2) {
551
+ var e = new KeychainError("keychain/windows-not-supported",
552
+ "CredentialManager PowerShell module not installed");
553
+ e.fallback = true;
554
+ throw e;
555
+ }
556
+ if (r.code === 44) return false;
557
+ if (r.code !== 0) {
558
+ throw new KeychainError("keychain/windows-remove-failed",
559
+ "PowerShell Remove-StoredCredential exited " + r.code + ": " +
560
+ r.stderr.toString("utf8").trim());
561
+ }
562
+ return true;
563
+ }
564
+
565
+ // ---- Audit emit ------------------------------------------------------------
566
+ // drop-silent — by design (audit is best-effort observability).
567
+
568
+ function _emit(action, outcome, metadata, auditOn) {
569
+ if (auditOn === false) return;
570
+ try {
571
+ audit().safeEmit({
572
+ action: action,
573
+ outcome: outcome,
574
+ metadata: metadata || {},
575
+ });
576
+ } catch (_e) { /* audit best-effort */ }
577
+ }
578
+
579
+ // ---- Backend selection -----------------------------------------------------
580
+
581
+ function _selectBackend(opts) {
582
+ if (opts && opts.preferFile === true) return "file";
583
+ return _detectBackend() || "file";
584
+ }
585
+
586
+ function _isFallbackError(e) {
587
+ // A KeychainError flagged with .fallback === true means the native
588
+ // tool reported "not installed / unavailable" rather than an
589
+ // operational failure. Promote to file fallback transparently.
590
+ return e instanceof FrameworkError && e.fallback === true;
591
+ }
592
+
593
+ // ---- Public surface --------------------------------------------------------
594
+
595
+ /**
596
+ * @primitive b.keychain.store
597
+ * @signature b.keychain.store(opts)
598
+ * @since 0.7.0
599
+ * @related b.keychain.retrieve, b.keychain.remove
600
+ *
601
+ * Persist a `(service, account) -> password` binding to the
602
+ * platform's native credential store, falling back to an encrypted
603
+ * file when no native backend is reachable. The password crosses to
604
+ * the native tool on stdin so it never appears in `/proc/<pid>/cmdline`
605
+ * or `ps`. Resolves to `{ stored: true, backend }` on success.
606
+ * Bad opts throw `KeychainError` synchronously.
607
+ *
608
+ * Set `preferFile: true` to skip native backend probing entirely (for
609
+ * deterministic CI / disposable container deployments).
610
+ *
611
+ * @opts
612
+ * {
613
+ * service: string, // required, no NUL/CR/LF bytes
614
+ * account: string, // required, no NUL/CR/LF bytes
615
+ * password: string, // required, non-empty
616
+ * fallbackFile?: string, // absolute path; required if file fallback may engage
617
+ * passphrase?: string, // required when fallbackFile engages (Argon2id-derived KEK)
618
+ * preferFile?: boolean, // default: false
619
+ * audit?: boolean, // default: true (emits keychain.stored)
620
+ * }
621
+ *
622
+ * @example
623
+ * await b.keychain.store({
624
+ * service: "blamejs/db",
625
+ * account: "primary",
626
+ * password: "s3cr3t",
627
+ * fallbackFile: "/var/lib/blamejs/keychain.enc",
628
+ * passphrase: process.env.BLAMEJS_KEYCHAIN_PASSPHRASE,
629
+ * });
630
+ * // → { stored: true, backend: "macos-security" }
631
+ */
632
+ async function store(opts) {
633
+ _validateCommonOpts(opts, "keychain.store");
634
+ validateOpts.requireNonEmptyString(opts.password, "password",
635
+ KeychainError, "keychain/bad-password");
636
+
637
+ var backend = _selectBackend(opts);
638
+ var auditOn = opts.audit !== false;
639
+
640
+ if (backend !== "file") {
641
+ try {
642
+ if (backend === "macos-security") await _macStore(opts.service, opts.account, opts.password);
643
+ else if (backend === "linux-secret-tool") await _linuxStore(opts.service, opts.account, opts.password);
644
+ else if (backend === "windows-credential") await _windowsStore(opts.service, opts.account, opts.password);
645
+ _emit("keychain.stored", "success", {
646
+ service: opts.service, account: opts.account, backend: backend,
647
+ }, auditOn);
648
+ return { stored: true, backend: backend };
649
+ } catch (e) {
650
+ if (!_isFallbackError(e)) {
651
+ _emit("keychain.stored", "failure", {
652
+ service: opts.service, account: opts.account, backend: backend,
653
+ code: e && e.code, message: e && e.message,
654
+ }, auditOn);
655
+ throw e;
656
+ }
657
+ // fallthrough to file fallback
658
+ }
659
+ }
660
+
661
+ _validateFallbackFile(opts.fallbackFile, "keychain.store");
662
+ var doc = await _readFile(opts.fallbackFile, opts.passphrase);
663
+ doc.entries[_bindingKey(opts.service, opts.account)] = String(opts.password);
664
+ await _writeFile(opts.fallbackFile, doc, opts.passphrase);
665
+ _emit("keychain.stored", "success", {
666
+ service: opts.service, account: opts.account, backend: "file",
667
+ }, auditOn);
668
+ return { stored: true, backend: "file" };
669
+ }
670
+
671
+ /**
672
+ * @primitive b.keychain.retrieve
673
+ * @signature b.keychain.retrieve(opts)
674
+ * @since 0.7.0
675
+ * @related b.keychain.store, b.keychain.remove
676
+ *
677
+ * Look up the password for `(service, account)` from the native
678
+ * credential store, falling back to the encrypted file when the
679
+ * native store has no entry or no native backend is reachable.
680
+ * Resolves to `{ password, backend }` on a hit, `null` on a clean
681
+ * miss. Native-tool failures surface as `KeychainError` with the
682
+ * tool's stderr included.
683
+ *
684
+ * @opts
685
+ * {
686
+ * service: string, // required
687
+ * account: string, // required
688
+ * fallbackFile?: string, // absolute path; required for file-backend lookup
689
+ * passphrase?: string, // required when fallbackFile engages
690
+ * preferFile?: boolean, // default: false
691
+ * audit?: boolean, // default: true (emits keychain.retrieved)
692
+ * }
693
+ *
694
+ * @example
695
+ * var got = await b.keychain.retrieve({
696
+ * service: "blamejs/db",
697
+ * account: "primary",
698
+ * fallbackFile: "/var/lib/blamejs/keychain.enc",
699
+ * passphrase: process.env.BLAMEJS_KEYCHAIN_PASSPHRASE,
700
+ * });
701
+ * // → { password: "s3cr3t", backend: "macos-security" } // or null on miss
702
+ */
703
+ async function retrieve(opts) {
704
+ _validateCommonOpts(opts, "keychain.retrieve");
705
+
706
+ var backend = _selectBackend(opts);
707
+ var auditOn = opts.audit !== false;
708
+
709
+ if (backend !== "file") {
710
+ try {
711
+ var pw = null;
712
+ if (backend === "macos-security") pw = await _macRetrieve(opts.service, opts.account);
713
+ else if (backend === "linux-secret-tool") pw = await _linuxRetrieve(opts.service, opts.account);
714
+ else if (backend === "windows-credential") pw = await _windowsRetrieve(opts.service, opts.account);
715
+ if (pw === null || pw === undefined) {
716
+ // Fall through to file fallback when the OS keychain has no
717
+ // entry — operators may have stored under file mode and later
718
+ // booted on a host with a native keychain.
719
+ } else {
720
+ _emit("keychain.retrieved", "success", {
721
+ service: opts.service, account: opts.account, backend: backend,
722
+ }, auditOn);
723
+ return { password: pw, backend: backend };
724
+ }
725
+ } catch (e) {
726
+ if (!_isFallbackError(e)) {
727
+ _emit("keychain.retrieved", "failure", {
728
+ service: opts.service, account: opts.account, backend: backend,
729
+ code: e && e.code, message: e && e.message,
730
+ }, auditOn);
731
+ throw e;
732
+ }
733
+ }
734
+ }
735
+
736
+ if (!opts.fallbackFile) {
737
+ _emit("keychain.retrieved", "success", {
738
+ service: opts.service, account: opts.account, backend: "none",
739
+ found: false,
740
+ }, auditOn);
741
+ return null;
742
+ }
743
+ _validateFallbackFile(opts.fallbackFile, "keychain.retrieve");
744
+ if (!atomicFile.exists(opts.fallbackFile)) {
745
+ _emit("keychain.retrieved", "success", {
746
+ service: opts.service, account: opts.account, backend: "file",
747
+ found: false,
748
+ }, auditOn);
749
+ return null;
750
+ }
751
+ var doc = await _readFile(opts.fallbackFile, opts.passphrase);
752
+ var bindingKey = _bindingKey(opts.service, opts.account);
753
+ var found = Object.prototype.hasOwnProperty.call(doc.entries, bindingKey);
754
+ if (!found) {
755
+ _emit("keychain.retrieved", "success", {
756
+ service: opts.service, account: opts.account, backend: "file",
757
+ found: false,
758
+ }, auditOn);
759
+ return null;
760
+ }
761
+ _emit("keychain.retrieved", "success", {
762
+ service: opts.service, account: opts.account, backend: "file",
763
+ }, auditOn);
764
+ return { password: doc.entries[bindingKey], backend: "file" };
765
+ }
766
+
767
+ /**
768
+ * @primitive b.keychain.remove
769
+ * @signature b.keychain.remove(opts)
770
+ * @since 0.7.0
771
+ * @related b.keychain.store, b.keychain.retrieve
772
+ *
773
+ * Delete the `(service, account)` binding from both the native
774
+ * credential store (when reachable) and the encrypted file fallback
775
+ * (when `fallbackFile` is supplied). Resolves to `true` when at least
776
+ * one backend held the binding, `false` on a no-op. The double-sweep
777
+ * matters because a binding may have been stored on a prior boot
778
+ * under a different backend than the current host advertises.
779
+ *
780
+ * @opts
781
+ * {
782
+ * service: string, // required
783
+ * account: string, // required
784
+ * fallbackFile?: string, // absolute path; required for file-backend cleanup
785
+ * passphrase?: string, // required when fallbackFile engages
786
+ * preferFile?: boolean, // default: false
787
+ * audit?: boolean, // default: true (emits keychain.removed)
788
+ * }
789
+ *
790
+ * @example
791
+ * var existed = await b.keychain.remove({
792
+ * service: "blamejs/db",
793
+ * account: "primary",
794
+ * fallbackFile: "/var/lib/blamejs/keychain.enc",
795
+ * passphrase: process.env.BLAMEJS_KEYCHAIN_PASSPHRASE,
796
+ * });
797
+ * // → true
798
+ */
799
+ async function remove(opts) {
800
+ _validateCommonOpts(opts, "keychain.remove");
801
+
802
+ var backend = _selectBackend(opts);
803
+ var auditOn = opts.audit !== false;
804
+
805
+ if (backend !== "file") {
806
+ try {
807
+ var ok = false;
808
+ if (backend === "macos-security") ok = await _macRemove(opts.service, opts.account);
809
+ else if (backend === "linux-secret-tool") ok = await _linuxRemove(opts.service, opts.account);
810
+ else if (backend === "windows-credential") ok = await _windowsRemove(opts.service, opts.account);
811
+ _emit("keychain.removed", ok ? "success" : "no-op", {
812
+ service: opts.service, account: opts.account, backend: backend,
813
+ }, auditOn);
814
+ // Also sweep file fallback if both could carry the binding.
815
+ if (opts.fallbackFile && atomicFile.exists(opts.fallbackFile)) {
816
+ try { await _removeFromFile(opts.fallbackFile, opts.service, opts.account, opts.passphrase); }
817
+ catch (_e) { /* file remove best-effort when native succeeded */ }
818
+ }
819
+ return ok;
820
+ } catch (e) {
821
+ if (!_isFallbackError(e)) {
822
+ _emit("keychain.removed", "failure", {
823
+ service: opts.service, account: opts.account, backend: backend,
824
+ code: e && e.code, message: e && e.message,
825
+ }, auditOn);
826
+ throw e;
827
+ }
828
+ }
829
+ }
830
+
831
+ if (!opts.fallbackFile || !atomicFile.exists(opts.fallbackFile)) {
832
+ _emit("keychain.removed", "no-op", {
833
+ service: opts.service, account: opts.account, backend: "file",
834
+ }, auditOn);
835
+ return false;
836
+ }
837
+ _validateFallbackFile(opts.fallbackFile, "keychain.remove");
838
+ var existed = await _removeFromFile(opts.fallbackFile, opts.service, opts.account, opts.passphrase);
839
+ _emit("keychain.removed", existed ? "success" : "no-op", {
840
+ service: opts.service, account: opts.account, backend: "file",
841
+ }, auditOn);
842
+ return existed;
843
+ }
844
+
845
+ async function _removeFromFile(fallbackFile, service, account, passphrase) {
846
+ var doc = await _readFile(fallbackFile, passphrase);
847
+ var bindingKey = _bindingKey(service, account);
848
+ if (!Object.prototype.hasOwnProperty.call(doc.entries, bindingKey)) return false;
849
+ delete doc.entries[bindingKey];
850
+ await _writeFile(fallbackFile, doc, passphrase);
851
+ return true;
852
+ }
853
+
854
+ // ---- Test seam -------------------------------------------------------------
855
+ // Reset the cached backend probe so a test can flip platform / PATH and
856
+ // re-run detection. NOT operator-facing.
857
+ function _clearBackendCacheForTest() { _cachedBackend = null; }
858
+
859
+ module.exports = {
860
+ store: store,
861
+ retrieve: retrieve,
862
+ remove: remove,
863
+ KeychainError: KeychainError,
864
+ _clearBackendCacheForTest: _clearBackendCacheForTest,
865
+ };