vaultkeeper 0.0.0 → 0.3.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.
@@ -0,0 +1,616 @@
1
+ /**
2
+ * Error hierarchy for vaultkeeper.
3
+ */
4
+ /** Base error for all vaultkeeper errors. */
5
+ declare class VaultError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * Thrown when the backend keychain or credential store is locked and requires
10
+ * user interaction (e.g. biometric prompt or password entry) before access can
11
+ * be granted.
12
+ */
13
+ declare class BackendLockedError extends VaultError {
14
+ /**
15
+ * Whether the lock can be resolved through an interactive user prompt.
16
+ * When `true`, callers may retry after prompting the user.
17
+ */
18
+ readonly interactive: boolean;
19
+ constructor(message: string, interactive: boolean);
20
+ }
21
+ /**
22
+ * Thrown when a hardware device (e.g. YubiKey or smart card) required for
23
+ * authentication is not currently connected.
24
+ */
25
+ declare class DeviceNotPresentError extends VaultError {
26
+ /**
27
+ * How long (in milliseconds) the operation waited for the device before
28
+ * giving up.
29
+ */
30
+ readonly timeoutMs: number;
31
+ constructor(message: string, timeoutMs: number);
32
+ }
33
+ /**
34
+ * Thrown when the user explicitly denies an authorization request for a
35
+ * secret access operation (e.g. cancels an OS permission dialog).
36
+ */
37
+ declare class AuthorizationDeniedError extends VaultError {
38
+ constructor(message: string);
39
+ }
40
+ /**
41
+ * Thrown when no configured backend is available or reachable.
42
+ * Inspect `reason` for a machine-readable cause and `attempted` for the list
43
+ * of backend types that were tried.
44
+ */
45
+ declare class BackendUnavailableError extends VaultError {
46
+ /**
47
+ * Machine-readable reason code describing why the backend is unavailable
48
+ * (e.g. `'none-enabled'`, `'all-failed'`).
49
+ */
50
+ readonly reason: string;
51
+ /**
52
+ * The backend type identifiers that were attempted before this error was
53
+ * thrown.
54
+ */
55
+ readonly attempted: string[];
56
+ constructor(message: string, reason: string, attempted: string[]);
57
+ }
58
+ /**
59
+ * Thrown when a required backend plugin (e.g. a third-party credential
60
+ * manager) is not installed on the current system.
61
+ */
62
+ declare class PluginNotFoundError extends VaultError {
63
+ /**
64
+ * The plugin package or binary name that was not found.
65
+ */
66
+ readonly plugin: string;
67
+ /**
68
+ * A URL pointing to installation instructions for the missing plugin.
69
+ */
70
+ readonly installUrl: string;
71
+ constructor(message: string, plugin: string, installUrl: string);
72
+ }
73
+ /**
74
+ * Thrown when a requested secret does not exist in the backend store.
75
+ */
76
+ declare class SecretNotFoundError extends VaultError {
77
+ constructor(message: string);
78
+ }
79
+ /**
80
+ * Thrown when a JWE token has passed its expiration time (`exp` claim).
81
+ */
82
+ declare class TokenExpiredError extends VaultError {
83
+ /**
84
+ * Whether the token can be refreshed by calling `setup()` again.
85
+ * When `true`, the secret still exists in the backend and a new token can be
86
+ * issued.
87
+ */
88
+ readonly canRefresh: boolean;
89
+ constructor(message: string, canRefresh: boolean);
90
+ }
91
+ /**
92
+ * Thrown when the encryption key that was used to create a JWE has since been
93
+ * rotated out of the grace period and can no longer be used for decryption.
94
+ */
95
+ declare class KeyRotatedError extends VaultError {
96
+ constructor(message: string);
97
+ }
98
+ /**
99
+ * Thrown when the encryption key referenced by a JWE's `kid` header has been
100
+ * explicitly revoked and is no longer available for decryption.
101
+ */
102
+ declare class KeyRevokedError extends VaultError {
103
+ constructor(message: string);
104
+ }
105
+ /**
106
+ * Thrown when a JWE token has been explicitly blocked (e.g. after a single-use
107
+ * token has already been consumed).
108
+ */
109
+ declare class TokenRevokedError extends VaultError {
110
+ constructor(message: string);
111
+ }
112
+ /**
113
+ * Thrown when a token with a finite `use` limit has been presented more times
114
+ * than the limit allows.
115
+ */
116
+ declare class UsageLimitExceededError extends VaultError {
117
+ constructor(message: string);
118
+ }
119
+ /**
120
+ * Thrown when the hash of an executable no longer matches the previously
121
+ * approved hash stored in the trust manifest (TOFU conflict).
122
+ *
123
+ * Callers must re-approve the executable before a new token can be issued for
124
+ * it.
125
+ */
126
+ declare class IdentityMismatchError extends VaultError {
127
+ /**
128
+ * The hash that was recorded in the trust manifest at approval time.
129
+ */
130
+ readonly previousHash: string;
131
+ /**
132
+ * The hash computed from the executable at the current moment.
133
+ */
134
+ readonly currentHash: string;
135
+ constructor(message: string, previousHash: string, currentHash: string);
136
+ }
137
+ /**
138
+ * Thrown during initialization when a required system dependency (e.g. OpenSSL
139
+ * or a native credential helper) is missing or incompatible.
140
+ */
141
+ declare class SetupError extends VaultError {
142
+ /**
143
+ * The name of the dependency that caused the setup failure.
144
+ */
145
+ readonly dependency: string;
146
+ constructor(message: string, dependency: string);
147
+ }
148
+ /**
149
+ * Thrown when a filesystem operation fails due to a permission or access
150
+ * problem (e.g. the config directory is not writable).
151
+ */
152
+ declare class FilesystemError extends VaultError {
153
+ /**
154
+ * The absolute path of the file or directory that caused the error.
155
+ */
156
+ readonly path: string;
157
+ /**
158
+ * The permission level that was required but not available
159
+ * (e.g. `'read'`, `'write'`, `'execute'`).
160
+ */
161
+ readonly permission: string;
162
+ constructor(message: string, filePath: string, permission: string);
163
+ }
164
+ /**
165
+ * Thrown when a key rotation is requested while a previous rotation is still
166
+ * within its grace period (i.e. the previous key has not yet been retired).
167
+ */
168
+ declare class RotationInProgressError extends VaultError {
169
+ constructor(message: string);
170
+ }
171
+
172
+ /**
173
+ * Shared types and interfaces for vaultkeeper.
174
+ */
175
+ /** Trust tier for executable identity verification. */
176
+ type TrustTier = 1 | 2 | 3;
177
+ /** Key status in the rotation lifecycle. */
178
+ type KeyStatus = 'current' | 'previous' | 'deprecated';
179
+ /** Status of a preflight check. */
180
+ type PreflightCheckStatus = 'ok' | 'missing' | 'version-unsupported';
181
+ /** Result of a preflight check for a single dependency. */
182
+ interface PreflightCheck {
183
+ /** Human-readable name of the dependency being checked. */
184
+ name: string;
185
+ /** Whether the dependency was found and is a supported version. */
186
+ status: PreflightCheckStatus;
187
+ /** The detected version string, if the dependency was found. */
188
+ version?: string | undefined;
189
+ /** Human-readable explanation of why the status is not `'ok'`. */
190
+ reason?: string | undefined;
191
+ }
192
+ /** Aggregated result from all preflight checks. */
193
+ interface PreflightResult {
194
+ /** Individual check results, one per dependency inspected. */
195
+ checks: PreflightCheck[];
196
+ /** `true` if all required checks passed and the system is ready. */
197
+ ready: boolean;
198
+ /** Non-fatal advisory messages about optional missing dependencies. */
199
+ warnings: string[];
200
+ /** Action items the user should complete before vaultkeeper will work. */
201
+ nextSteps: string[];
202
+ }
203
+ /** Response from a vault access operation. */
204
+ interface VaultResponse {
205
+ /** Replacement JWE if key was rotated */
206
+ rotatedJwt?: string | undefined;
207
+ /** Current key status */
208
+ keyStatus: KeyStatus;
209
+ }
210
+ /**
211
+ * Request for delegated HTTP fetch.
212
+ *
213
+ * String values in `url`, `headers`, and `body` may include the placeholder
214
+ * `{{secret}}`, which is replaced with the actual secret value immediately
215
+ * before the request is sent.
216
+ */
217
+ interface FetchRequest {
218
+ /**
219
+ * The target URL. May contain `{{secret}}` which is replaced with the secret
220
+ * value before the fetch is executed (e.g. for API-key-in-URL patterns).
221
+ */
222
+ url: string;
223
+ /** HTTP method (defaults to `'GET'` when omitted). */
224
+ method?: string | undefined;
225
+ /**
226
+ * Request headers. Any header value may contain `{{secret}}`, which is
227
+ * replaced with the secret value before the request is sent.
228
+ */
229
+ headers?: Record<string, string> | undefined;
230
+ /**
231
+ * Request body. May contain `{{secret}}`, which is replaced with the secret
232
+ * value before the request is sent.
233
+ */
234
+ body?: string | undefined;
235
+ }
236
+ /**
237
+ * Request for delegated command execution.
238
+ *
239
+ * String values in `args` and `env` may include the placeholder `{{secret}}`,
240
+ * which is replaced with the actual secret value immediately before the command
241
+ * is spawned.
242
+ */
243
+ interface ExecRequest {
244
+ /** The command (binary) to execute. */
245
+ command: string;
246
+ /**
247
+ * Command-line arguments. Any argument may contain `{{secret}}`, which is
248
+ * replaced with the secret value before the command is spawned.
249
+ */
250
+ args?: string[] | undefined;
251
+ /**
252
+ * Additional environment variables to merge into the child process
253
+ * environment. Any value may contain `{{secret}}`, which is replaced with
254
+ * the secret value before the command is spawned.
255
+ */
256
+ env?: Record<string, string> | undefined;
257
+ /** Working directory for the spawned process. */
258
+ cwd?: string | undefined;
259
+ }
260
+ /** Result from delegated command execution. */
261
+ interface ExecResult {
262
+ /** Captured standard output from the process. */
263
+ stdout: string;
264
+ /** Captured standard error from the process. */
265
+ stderr: string;
266
+ /** Process exit code. */
267
+ exitCode: number;
268
+ }
269
+ /**
270
+ * Callback-based secret accessor with auto-zeroing.
271
+ *
272
+ * The accessor is backed by a revocable Proxy. Calling `read()` passes a
273
+ * `Buffer` containing the secret to the callback, then zeroes the buffer after
274
+ * the callback returns. The accessor can only be read once; a second call
275
+ * throws.
276
+ */
277
+ interface SecretAccessor {
278
+ /**
279
+ * Read the secret value via a callback.
280
+ *
281
+ * The `buf` argument is a temporary `Buffer` containing the secret encoded
282
+ * as UTF-8. The buffer is zeroed immediately after the callback returns, so
283
+ * callers must not store a reference to it beyond the callback scope.
284
+ *
285
+ * @param callback - Function that receives the secret buffer.
286
+ * @throws {Error} If the accessor has already been consumed.
287
+ */
288
+ read(callback: (buf: Buffer) => void): void;
289
+ }
290
+ /** Vaultkeeper configuration file structure. */
291
+ interface VaultConfig {
292
+ /** Config schema version. Currently must be `1`. */
293
+ version: number;
294
+ /** Ordered list of backend configurations. The first enabled backend is used. */
295
+ backends: BackendConfig[];
296
+ /** Key rotation policy. */
297
+ keyRotation: {
298
+ /**
299
+ * Number of days the previous key remains valid for decryption after a
300
+ * rotation event.
301
+ */
302
+ gracePeriodDays: number;
303
+ };
304
+ /** Default values applied to `setup()` when options are not explicitly provided. */
305
+ defaults: {
306
+ /** Default JWE time-to-live in minutes. */
307
+ ttlMinutes: number;
308
+ /** Default trust tier for executable identity verification. */
309
+ trustTier: TrustTier;
310
+ };
311
+ /** Development mode configuration. When present, identity checks are relaxed for listed executables. */
312
+ developmentMode?: {
313
+ /** Paths of executables that bypass identity verification in development mode. */
314
+ executables: string[];
315
+ } | undefined;
316
+ }
317
+ /** Configuration for a single backend. */
318
+ interface BackendConfig {
319
+ /** Backend type identifier (e.g. `'keychain'`, `'file'`, `'1password'`). */
320
+ type: string;
321
+ /** Whether this backend is active. Only enabled backends are considered during initialization. */
322
+ enabled: boolean;
323
+ /** Whether this backend is provided by an external plugin rather than built in. */
324
+ plugin?: boolean | undefined;
325
+ /** Filesystem path used by file-based backends. */
326
+ path?: string | undefined;
327
+ }
328
+
329
+ /**
330
+ * Backend abstraction layer types for vaultkeeper.
331
+ */
332
+ /**
333
+ * Factory function for creating a SecretBackend instance.
334
+ * @public
335
+ */
336
+ type BackendFactory = () => SecretBackend;
337
+ /**
338
+ * Abstraction interface for all secret storage backends.
339
+ *
340
+ * @remarks
341
+ * Each backend implementation must handle its own availability check and
342
+ * secret lifecycle (store, retrieve, delete, exists).
343
+ *
344
+ * @public
345
+ */
346
+ interface SecretBackend {
347
+ /** Unique type identifier for this backend. */
348
+ readonly type: string;
349
+ /** Human-readable display name for this backend. */
350
+ readonly displayName: string;
351
+ /**
352
+ * Check whether this backend is available on the current system.
353
+ * @returns true if the backend can be used, false otherwise
354
+ */
355
+ isAvailable(): Promise<boolean>;
356
+ /**
357
+ * Store a secret under the given id.
358
+ * @param id - Unique identifier for the secret
359
+ * @param secret - The secret value to store
360
+ */
361
+ store(id: string, secret: string): Promise<void>;
362
+ /**
363
+ * Retrieve a secret by id.
364
+ * @param id - Unique identifier for the secret
365
+ * @returns The stored secret value
366
+ * @throws SecretNotFoundError if the secret does not exist
367
+ */
368
+ retrieve(id: string): Promise<string>;
369
+ /**
370
+ * Delete a secret by id.
371
+ * @param id - Unique identifier for the secret
372
+ * @throws SecretNotFoundError if the secret does not exist
373
+ */
374
+ delete(id: string): Promise<void>;
375
+ /**
376
+ * Check whether a secret exists for the given id.
377
+ * @param id - Unique identifier for the secret
378
+ * @returns true if the secret exists, false otherwise
379
+ */
380
+ exists(id: string): Promise<boolean>;
381
+ }
382
+ /**
383
+ * Backend that can enumerate stored secret IDs.
384
+ * @public
385
+ */
386
+ interface ListableBackend extends SecretBackend {
387
+ /**
388
+ * List IDs of all secrets managed by this backend.
389
+ * @returns Array of secret identifiers
390
+ */
391
+ list(): Promise<string[]>;
392
+ }
393
+ /**
394
+ * Type guard for backends that support listing.
395
+ * @public
396
+ */
397
+ declare function isListableBackend(backend: SecretBackend): backend is ListableBackend;
398
+
399
+ /**
400
+ * Registry for secret backend implementations.
401
+ *
402
+ * @remarks
403
+ * The registry maintains a mapping of backend types to factory functions,
404
+ * allowing dynamic creation of backends based on configuration.
405
+ */
406
+
407
+ /**
408
+ * Registry for secret backend implementations.
409
+ *
410
+ * @remarks
411
+ * The registry allows registration of custom backends and provides
412
+ * a factory method to create backend instances from a type string.
413
+ *
414
+ * Note: This class is used as a namespace for static methods.
415
+ * @public
416
+ */
417
+ declare class BackendRegistry {
418
+ private static backends;
419
+ /**
420
+ * Register a backend factory.
421
+ * @param type - Backend type identifier
422
+ * @param factory - Factory function to create backend instances
423
+ */
424
+ static register(type: string, factory: BackendFactory): void;
425
+ /**
426
+ * Create a backend instance by type.
427
+ * @param type - Backend type identifier
428
+ * @returns A SecretBackend instance
429
+ * @throws Error if the backend type is not registered
430
+ */
431
+ static create(type: string): SecretBackend;
432
+ /**
433
+ * Get all registered backend type identifiers.
434
+ * @returns Array of backend type identifiers
435
+ */
436
+ static getTypes(): string[];
437
+ /**
438
+ * Returns backend types that are available on the current system.
439
+ *
440
+ * @remarks
441
+ * Creates each registered backend via its factory, calls `isAvailable()`,
442
+ * and returns only the type identifiers whose backend reports availability.
443
+ * If a backend's `isAvailable()` call throws, that backend is excluded from
444
+ * the result rather than propagating the error.
445
+ *
446
+ * @returns Promise resolving to an array of available backend type identifiers
447
+ * @public
448
+ */
449
+ static getAvailableTypes(): Promise<string[]>;
450
+ }
451
+
452
+ /**
453
+ * Capability token management.
454
+ *
455
+ * Tokens are backed by a `WeakMap` whose keys are `CapabilityToken` instances,
456
+ * so the actual claims are never reachable from outside this module. Private
457
+ * class fields enforce that no property on the token object leaks data.
458
+ */
459
+
460
+ /** Opaque capability token. Claims are inaccessible without `validateCapabilityToken`. */
461
+ declare class CapabilityToken {
462
+ #private;
463
+ constructor();
464
+ /**
465
+ * Return a non-enumerable identifier for debugging purposes only.
466
+ * Does NOT expose claims.
467
+ */
468
+ toString(): string;
469
+ }
470
+
471
+ /**
472
+ * VaultKeeper main class — wires together all vaultkeeper subsystems.
473
+ */
474
+
475
+ /** Options for initializing VaultKeeper. */
476
+ interface VaultKeeperOptions {
477
+ /** Override the config directory. */
478
+ configDir?: string | undefined;
479
+ /** Supply config directly, skipping file load. */
480
+ config?: VaultConfig | undefined;
481
+ /** Skip the doctor preflight check. */
482
+ skipDoctor?: boolean | undefined;
483
+ }
484
+ /** Options for the setup operation. */
485
+ interface SetupOptions {
486
+ /** TTL in minutes for the JWE. */
487
+ ttlMinutes?: number | undefined;
488
+ /** Usage limit (null for unlimited). */
489
+ useLimit?: number | null | undefined;
490
+ /** Executable path for identity binding. Use "dev" for dev mode. */
491
+ executablePath?: string | undefined;
492
+ /** Trust tier override. */
493
+ trustTier?: TrustTier | undefined;
494
+ /** Backend type to use. */
495
+ backendType?: string | undefined;
496
+ }
497
+ /**
498
+ * Main entry point for vaultkeeper. Orchestrates backends, keys, JWE tokens,
499
+ * identity verification, and access patterns.
500
+ */
501
+ declare class VaultKeeper {
502
+ #private;
503
+ private constructor();
504
+ /**
505
+ * Initialize a new VaultKeeper instance.
506
+ * Runs doctor checks (unless skipped), loads config, and sets up the key manager.
507
+ */
508
+ static init(options?: VaultKeeperOptions): Promise<VaultKeeper>;
509
+ /** Run doctor checks without full initialization. */
510
+ static doctor(): Promise<PreflightResult>;
511
+ /**
512
+ * Store a secret and return a JWE token that encapsulates it.
513
+ *
514
+ * @param secretName - Identifier for the secret
515
+ * @param options - Setup options
516
+ * @returns Compact JWE string
517
+ */
518
+ setup(secretName: string, options?: SetupOptions): Promise<string>;
519
+ /**
520
+ * Decrypt a JWE, validate claims, verify executable identity, and return
521
+ * an opaque CapabilityToken.
522
+ *
523
+ * @param jwe - Compact JWE string from setup()
524
+ * @returns Opaque capability token for use with fetch/exec/getSecret
525
+ */
526
+ authorize(jwe: string): Promise<{
527
+ token: CapabilityToken;
528
+ response: VaultResponse;
529
+ }>;
530
+ /**
531
+ * Execute a delegated HTTP fetch, injecting the secret from the token.
532
+ *
533
+ * The secret value is substituted for every `{{secret}}` placeholder found
534
+ * in `request.url`, `request.headers`, and `request.body` before the fetch
535
+ * is executed. The raw secret is never exposed in the return value.
536
+ *
537
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
538
+ * @param request - The fetch request template. Use `{{secret}}` as a
539
+ * placeholder wherever the secret value should be injected.
540
+ * @returns The `Response` from the underlying `fetch()` call, together with
541
+ * the vault metadata (`vaultResponse`).
542
+ * @throws {Error} If `token` is invalid or was not created by this vault
543
+ * instance.
544
+ */
545
+ fetch(token: CapabilityToken, request: FetchRequest): Promise<{
546
+ response: Response;
547
+ vaultResponse: VaultResponse;
548
+ }>;
549
+ /**
550
+ * Execute a delegated command, injecting the secret from the token.
551
+ *
552
+ * The secret value is substituted for every `{{secret}}` placeholder found
553
+ * in `request.args` and `request.env` values before the process is spawned.
554
+ * The raw secret is never exposed in the return value.
555
+ *
556
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
557
+ * @param request - The exec request template. Use `{{secret}}` as a
558
+ * placeholder wherever the secret value should be injected.
559
+ * @returns The command result (`stdout`, `stderr`, `exitCode`) together with
560
+ * the vault metadata (`vaultResponse`).
561
+ * @throws {Error} If `token` is invalid or was not created by this vault
562
+ * instance.
563
+ */
564
+ exec(token: CapabilityToken, request: ExecRequest): Promise<{
565
+ result: ExecResult;
566
+ vaultResponse: VaultResponse;
567
+ }>;
568
+ /**
569
+ * Create a controlled-direct `SecretAccessor` from a capability token.
570
+ *
571
+ * The accessor wraps the secret in a single-use, auto-zeroing `Buffer`. The
572
+ * secret is accessible only through the `read()` callback and is zeroed
573
+ * immediately after the callback returns.
574
+ *
575
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
576
+ * @returns A `SecretAccessor` that can be read exactly once.
577
+ * @throws {Error} If `token` is invalid or was not created by this vault
578
+ * instance.
579
+ */
580
+ getSecret(token: CapabilityToken): SecretAccessor;
581
+ /**
582
+ * Rotate the current encryption key.
583
+ *
584
+ * The previous key remains valid for decryption during the grace period
585
+ * configured in `keyRotation.gracePeriodDays`. JWEs presented during the
586
+ * grace period return a `rotatedJwt` in the `VaultResponse` so callers can
587
+ * persist the updated token.
588
+ *
589
+ * @throws {RotationInProgressError} If a rotation is already in progress
590
+ * (i.e. a previous key is still within its grace period).
591
+ */
592
+ rotateKey(): Promise<void>;
593
+ /**
594
+ * Emergency key revocation — invalidates the previous key immediately.
595
+ *
596
+ * After revocation, any JWE that was encrypted with the revoked key will
597
+ * be permanently unreadable. A new encryption key is generated automatically
598
+ * so that `setup()` can be called immediately after revocation.
599
+ */
600
+ revokeKey(): Promise<void>;
601
+ /**
602
+ * Add or remove an executable from the development-mode whitelist.
603
+ *
604
+ * When an executable is in the development-mode list, identity verification
605
+ * (TOFU hash checking) is skipped for that executable during `setup()`. This
606
+ * is intended for local development workflows where the binary changes
607
+ * frequently.
608
+ *
609
+ * @param executablePath - Absolute path to the executable to add or remove.
610
+ * @param enabled - Pass `true` to add the executable to the list, `false`
611
+ * to remove it.
612
+ */
613
+ setDevelopmentMode(executablePath: string, enabled: boolean): Promise<void>;
614
+ }
615
+
616
+ export { AuthorizationDeniedError, type BackendConfig, type BackendFactory, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, type ExecRequest, type ExecResult, type FetchRequest, FilesystemError, IdentityMismatchError, KeyRevokedError, KeyRotatedError, type KeyStatus, type ListableBackend, PluginNotFoundError, type PreflightCheck, type PreflightCheckStatus, type PreflightResult, RotationInProgressError, type SecretAccessor, type SecretBackend, SecretNotFoundError, SetupError, type SetupOptions, TokenExpiredError, TokenRevokedError, type TrustTier, UsageLimitExceededError, type VaultConfig, VaultError, VaultKeeper, type VaultKeeperOptions, type VaultResponse, isListableBackend };