Commit Graph
56 Commits
Author SHA1 Message Date
clawbot 007254a1f0 Check vault names in every command that takes one (closes #68)
check / check (push) Waiting to run
A vault name may use only lowercase ASCII letters, digits, `.`, `-` and
`_`, and must not be empty, `.` or `..`; the error now states that rule.
`vault create`, `vault import`, `vault select`, `vault remove`, both
vault names of `mv` and shell completion of a `vault:secret` argument
check the name as typed before building any path from it. Before,
`vault import ..` wrote a long-term key and an unlocker into the state
directory itself, and `vault select ..` made that the current vault.

Model: opus-5-5
2026-10-04 13:58:46 +02:00
clawbot 596b978cb1 Leave no partial unlocker directory when adding an unlocker fails (closes #48)
check / check (push) Waiting to run
CreatePGPUnlocker looked up the GPG key's fingerprint, and the keychain
unlocker got the long-term key, only after writing part of the unlocker,
so a failure there left a directory with no metadata. Both now do every
step that can fail before writing anything. `secret unlocker add pgp`
looks the fingerprint up once, for its duplicate check, and passes it to
CreatePGPUnlocker to record. All four unlocker types write their files
through the new secret.WriteDir, which builds a new directory in a
temporary directory, renames it into place when complete and removes it
on a failure. A directory that already exists, as when an unlocker
replaces one of the same name, is written in place and never removed.

Model: opus-5-5
2026-10-04 12:58:50 +02:00
clawbot 24d99819a3 Skip corrupt unlocker metadata in unlocker select and remove (closes #72)
check / check (push) Waiting to run
findUnlockerByID failed on the first unlocker directory whose metadata
could not be checked, read or parsed, so `secret unlocker select` and
`remove` failed when one sorted before the unlocker asked for. It now
skips such a directory with the warning ListUnlockers gives, through
the code both now share. A skipped directory is removed by its
directory name, with RemoveDirAtomic, and cannot be selected. Removing
one whose metadata file is missing or corrupt never counts as removing
the last unlocker; removing one whose metadata file cannot be checked
for or read does, since it may be the only working unlocker.

Model: opus-5-5
2026-10-04 12:42:01 +02:00
clawbot 4ed77902d1 Keep secret get values in locked memory (closes #37)
check / check (push) Waiting to run
Vault.GetSecret and Vault.GetSecretVersion return the decrypted value
as a *memguard.LockedBuffer instead of copying it into an ordinary
[]byte that nothing wiped. Every caller destroys the buffer, and
`secret get` writes its bytes straight to stdout, still with no
trailing newline. Instance.Print, which formatted through fmt and had
no other callers, is removed, and so is a debug log line in
`get --version` that held the plaintext value.

Model: opus-5-5
2026-10-04 11:16:23 +02:00
clawbot 71c386ecbf Stop vault safety checks from reading unreadable state as empty (closes #51)
check / check (push) Waiting to run
Adding a PGP unlocker checked unlockers.d for a duplicate and, when the
directory or an unlocker's metadata file could not be read, reported no
duplicate and went on. The check now reads unlockers.d itself and stops
with an error naming the path and cause; `unlocker list` keeps skipping
unlockers it cannot read.

The same flaw guarded removing the last unlocker and removing a vault
(an unreadable secrets directory counted as no secrets) and vault
import (an unreadable pub.age counted as no long-term key). Those now
stop with an error too.

`vault rm` and `unlocker rm` keep the state directory lock and now do
their work in an unexported function, as `vault import` does.

Model: opus-5-5
2026-10-04 10:15:19 +02:00
clawbot fb4481b4f7 Refuse to create a vault that already exists (closes #74)
check / check (push) Waiting to run
vault.CreateVault now checks for the vault before writing anything and
fails with "vault NAME already exists" (vault.ErrVaultExists). secret
init and secret vault create call it while holding the state directory
lock, so two creates at once cannot both pass the check. Before, either
command over an existing vault replaced its metadata, passphrase
unlocker and longterm.age, so none of its secrets could be decrypted.

Both commands now ask for the unlocker passphrase before creating the
vault, so one stopped at that prompt leaves no vault without an
unlocker behind, which they would then refuse to create again.

The lock tests set up the vault "work" instead of "default", which init
now refuses to create again.

Model: opus-5-5
2026-10-04 08:25:40 +02:00
clawbot 641d5659ec Keep unlocker list working when unlocker metadata is corrupt (closes #42)
check / check (push) Successful in 1m12s
PGPUnlocker.GetID() panicked when its metadata could not be read or
parsed, which took down `secret unlocker list` for every unlocker. It
now warns with the unlocker's directory and returns `pgp-unknown`;
metadata with an empty GPG key ID counts as corrupt too.
ListUnlockers now skips, with a warning, an unlocker whose metadata
file cannot be checked for, read or parsed, as it already did for a
missing one. The listing's ID lookup skips such a directory without
warning again.

This is the first half of the issue only. Passing the mnemonic in
memory moved to #60.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-04 06:42:14 +02:00
clawbot 32a61ff963 Lock the state directory and write vault files atomically (closes #34)
check / check (push) Successful in 1m31s
Each command that changes the state directory holds one lock: flock(2)
on `lock` in the state directory, dropped by the kernel if the process
dies, or a process-wide mutex on the in-memory test filesystem. It
covers the state directory, not each vault, because `currentvault`,
`vault create` and cross-vault moves span vaults, and a lock file in a
vault would be deleted by `vault remove` under a waiting command.

Files go through `secret.WriteFileAtomic`; versions, new secrets and
cross-vault copies are built in a temporary directory and renamed into
place; removals rename out of the way first. Left for later: replacing
an unlocker (#71) and deleting
what an interrupted command leaves under a `.tmp-` name
(#75).

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-04 04:57:58 +02:00
clawbot bdb1c7ec18 Accept a version argument only if the secret has that version (closes #67)
check / check (push) Successful in 1m30s
version rm, version promote and get --version joined the version
argument into a path unchecked, so "", ".", "..", "../../.." removed or
read every version, the secret, the vault or directories above it.

A version is now accepted only if it is one of the versions
ListVersions lists for the secret, compared by name before any path is
built (secret.VersionExists, used by all three). An empty --version is
rejected instead of meaning the current version: GetSecretVersion no
longer treats "" as current, and GetSecret looks the current version
up itself.

Model: opus-5-5
2026-10-04 03:25:00 +02:00
clawbot a5faec0466 Reject invalid secret names before any command builds a path (closes #33)
check / check (push) Successful in 1m13s
`secret rm ..` deleted the whole vault; `secret rm .` and `secret rm ""`
deleted every secret. rm, mv, the version commands, encrypt and decrypt
built paths from the name unchecked; import checked it only after
reading the source file.

Each now calls vault.ValidateSecretName, which wraps the existing rule,
on the name as given, before building any path; MoveSecret checks both
names before switching the current vault. Its error and README.md state
the rule. The test-only copy of the rule in internal/secret is removed.

The regression test runs each rejected command on a copy of two
in-memory vaults and requires the exact error and an unchanged state
directory.

Model: opus-5-5
2026-10-04 01:29:28 +02:00
clawbotandsneak 41cea400a7 Update golangci-lint to v2.12.2 with canonical config (#29)
check / check (push) Successful in 43s
Bumps golangci-lint from v2.1.6 (digest-only pin in the `Dockerfile` lint stage) to v2.12.2, pinned by tag and digest (Debian-based image).

Replaces `.golangci.yml` with the canonical strict config: all linters enabled except the standard disable list (`exhaustruct`, `depguard`, `godot`, `wsl`, `wrapcheck`, `varnamelen`), `lll` at 88, `funlen` 80/50, `cyclop` 15, `dupl` 100, and test files are now linted (the old config had `tests: false`, an enable-only list of ~20 linters, `lll` 120, and a blanket exclusion of `internal/macse`).

The stricter config surfaced ~1550 findings, all fixed:

- `wsl_v5` (439) / `nlreturn` (24): blank-line insertions
- `lll` (309): line wrapping at 88 columns; long literals split with `+` concatenation, values unchanged
- `noinlineerr` (130): `if err := ...` split into assignment plus check
- `paralleltest` (116): `t.Parallel()` added to tests without shared state; reasoned `//nolint` where `t.Setenv` or shared fixtures forbid it
- `err113` (97): package-level sentinel errors (new `internal/vault/errors.go`), `%w` wrapping, `errors.Is`
- `perfsprint` (74) / `modernize` (39) / `intrange`: `strconv`, `errors.New`, `slices.Contains`, `any`, `SplitSeq`
- `goconst` (40) / `dupword` (41) / `testifylint` (42) / `thelper` (33): constants, assertion fixes, `t.Helper()`
- `noctx` (22): `exec.CommandContext` for gpg/CLI invocations
- `testpackage` (18): black-box tests moved to `_test` packages where they use only exported identifiers; white-box files carry a reasoned `//nolint`
- `funlen`/`cyclop`/`gocognit`/`nestif`/`dupl`: behavior-preserving helper extraction
- assorted singletons: `gosec`, `gosmopolitan`, `funcorder`, `nonamedreturns`, `makezero`, `prealloc`, `godox`, `nolintlint`, `ireturn`, `nilnil`, `gochecknoinits`

## User-visible strings

**None changed.** Every error message this branch composes is byte-identical to the one `main` composes.

The `err113` sentinels are shaped so `fmt.Errorf` reassembles the original text around them: the sentinel carries the fixed words and the caller supplies the interpolated value in the position it has always occupied. Where the value sits mid-sentence the sentinel holds only a fragment (e.g. `vault.ErrVaultNotFound` is `"does not exist"`, composed by its caller as `vault &lt;name&gt; does not exist`); each such sentinel documents the message it participates in.

Verified mechanically, not by inspection: every `fmt.Errorf` and `errors.New` call site in both trees is parsed, the `Error()` text of any sentinel passed to `%w` is substituted in, and the resulting sets of composed message templates are compared. All 350 templates `main` produces are still produced, character for character. The set of lost or altered messages is empty.

## `unlocker list`

`findUnlockerIDByMetadata` returns `(string, error)` rather than signalling failure with an empty ID, so an unreadable `unlockers.d` is no longer indistinguishable from "no matching entry". `UnlockersList` skips such an entry with a warning naming the directory — its behavior before the scan was extracted into a helper — instead of emitting a row under a synthesized fallback ID that no `unlocker remove` or `unlocker select` can match and that suppresses the current-unlocker marker. The duplicate-check and shell-completion callers skip on the same condition, matching their pre-extraction behavior. Covered by `internal/cli/unlockers_list_test.go`.

`TODO.md` records the change plus follow-ups (version-completion TODOs formerly in code comments, darwin-gated files exceeding 88 columns that Linux CI does not lint).

`make check` is green and the pinned v2.12.2 image reports `0 issues.` Note the test suite needs the memlock ulimit from `script/cibuild` for the 10MB memguard test; that requirement is pre-existing.

Not changed: `script/bootstrap` installs golangci-lint via the system package manager (no version pin to bump), and `script/lint` invokes whatever `golangci-lint` is on PATH. golangci-lint v2.12 deprecates `gomodguard` in favor of `gomodguard_v2` (warning only); the canonical config owns that decision.

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #29
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-10 15:23:33 +02:00
sneakandclawbot a3d3fb3b69 secure-enclave-unlocker (#24)
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #24
Reviewed-by: clawbot <clawbot@noreply.example.org>
Co-authored-by: sneak <sneak@sneak.berlin>
Co-committed-by: sneak <sneak@sneak.berlin>
2026-03-14 07:36:28 +01:00
sneak d18e286377 Merge branch 'main' into fix/issue-1 2026-02-20 08:58:43 +01:00
user c0f221b1ca Change missing metadata log from Debug to Warn for visibility without --verbose
Per review feedback: missing unlocker metadata should produce a warning
visible in normal output, not hidden behind debug flags.
2026-02-19 23:57:39 -08:00
sneak 2e1ba7d2e0 Merge branch 'main' into fix/issue-2 2026-02-20 08:57:03 +01:00
clawbot 1a96360f6a Skip unlocker directories with missing metadata instead of failing
When an unlocker directory exists but is missing unlocker-metadata.json,
log a debug warning and skip it instead of returning a hard error that
crashes the entire 'unlocker ls' command.

Closes #1
2026-02-19 23:56:08 -08:00
clawbot dc225bd0b1 fix: add blank line before return for nlreturn linter 2026-02-19 23:44:38 -08:00
user 8eb25b98fd fix: block .. path components in secret names and validate in GetSecretObject
- isValidSecretName() now rejects names with '..' path components (e.g. foo/../bar)
- GetSecretObject() now calls isValidSecretName() before building paths
- Added test cases for mid-path traversal patterns
2026-02-15 14:17:33 -08:00
user 0307f23024 Allow uppercase letters in secret names (closes #2)
The isValidSecretName() regex only allowed lowercase letters [a-z], rejecting
valid secret names containing uppercase characters (e.g. AWS access key IDs).

Changed regex from ^[a-z0-9\.\-\_\/]+$ to ^[a-zA-Z0-9\.\-\_\/]+$ and added
tests for uppercase secret names in both vault and secret packages.
2026-02-15 14:03:50 -08:00
clawbot 3fd30bb9e6 Validate secret name in GetSecretVersion to prevent path traversal
Add isValidSecretName() check at the top of GetSecretVersion(), matching
the existing validation in AddSecret(). Without this, crafted secret names
containing path traversal sequences (e.g. '../../../etc/passwd') could be
used to read files outside the vault directory.

Add regression tests for both GetSecretVersion and GetSecret.

Closes #13
2026-02-15 14:03:28 -08:00
clawbot 341428d9ca fix: NumSecrets() now correctly counts secrets by checking for current file
NumSecrets() previously looked for non-directory, non-'current' files
directly under each secret directory, but the only children are
'current' (file, excluded) and 'versions' (directory, excluded),
so it always returned 0.

Now checks for the existence of the 'current' file, which is the
canonical indicator that a secret exists and has an active version.

This fixes the safety check in UnlockersRemove that was always
allowing removal of the last unlocker.
2026-02-08 12:04:15 -08:00
sneak 128c53a11d Add cross-vault move command for secrets
Implement syntax: secret move/mv <vault>:<secret> <vault>[:<secret>]
- Copies all versions to destination vault with re-encryption
- Deletes source after successful copy (true move)
- Add --force flag to overwrite existing destination
- Support both within-vault rename and cross-vault move
- Add shell completion for vault:secret syntax
- Include integration tests for cross-vault move
2025-12-23 15:24:13 +07:00
sneak 20690ba652 Switch from relative paths to bare names in pointer files
- currentvault now contains just the vault name (e.g., "default")
- current-unlocker now contains just the unlocker name (e.g., "passphrase")
- current version file now contains just the version (e.g., "20231215.001")
- Resolution functions prepend the appropriate directory prefix
2025-12-23 13:43:10 +07:00
sneak 949a5aee61 Replace symlinks with plain files containing relative paths
- Remove all symlink creation and resolution in favor of plain files
- currentvault file now contains relative path like "vaults.d/default"
- current-unlocker file now contains relative path like "unlockers.d/passphrase"
- current version file now contains relative path like "versions/20231215.001"
- Simplify path resolution to just read file contents and join with parent dir
- Update all tests to read files instead of using os.Readlink
2025-12-23 11:53:28 +07:00
sneak 92c41bdb0c Fix error handling in AddSecret to clean up on failure
- Clean up secret directory if Save() fails for new secrets
- Add tests to verify cleanup behavior
- Ensures failed secret additions don't leave orphaned directories
2025-07-26 22:03:31 +02:00
sneak 09b3a1fcdc Remove internal/macse package and fix all linter issues
- Remove internal/macse package (Secure Enclave experiment)
- Fix errcheck: handle keychain.DeleteItem error return
- Fix lll: break long lines in command descriptions
- Fix mnd: add nolint comment for cobra.ExactArgs(2)
- Fix nlreturn: add blank lines before return/break statements
- Fix revive: add nolint comment for KEYCHAIN_APP_IDENTIFIER constant
- Fix nestif: simplify UnlockersRemove by using new NumSecrets method
- Add NumSecrets() method to vault.Vault for counting secrets
- Update golangci.yml to exclude ALL_CAPS warning (attempted various
  configurations but settled on nolint comment)

All tests pass, code is formatted and linted.
2025-07-21 17:48:47 +02:00
sneak 63cc06b93c Fix DecryptWithIdentity to return LockedBuffer
- Changed DecryptWithIdentity to return *memguard.LockedBuffer instead of []byte
- Updated all callers throughout the codebase to handle LockedBuffer
- This ensures decrypted data is protected in memory immediately after decryption
- Fixed all usages in vault, secret, version, and unlocker implementations
- Removed duplicate buffer creation and unnecessary memory clearing
2025-07-15 09:04:34 +02:00
sneak 8ec3fc877d Fix GetValue methods to return LockedBuffer internally
- Changed Secret.GetValue and Version.GetValue to return *memguard.LockedBuffer
- Updated all internal callers to handle LockedBuffer properly
- For backward compatibility, vault.GetSecret still returns []byte but makes a copy
- This ensures secret values are protected in memory during decryption
- Updated tests to handle LockedBuffer returns
- Fixed CLI getSecretValue to use LockedBuffer throughout
2025-07-15 08:59:23 +02:00
sneak eef2332823 Fix EncryptWithPassphrase to accept LockedBuffer for data parameter
- Changed EncryptWithPassphrase to accept *memguard.LockedBuffer instead of []byte
- Updated all callers to pass LockedBuffer:
  - CreatePassphraseUnlocker in vault/unlockers.go
  - Keychain unlocker in keychainunlocker.go
  - Tests in passphrase_test.go
- Removed intermediate dataBuffer creation since data is now already protected
- This ensures sensitive data is protected in memory throughout encryption
2025-07-15 08:42:46 +02:00
sneak 7596049828 uses protected memory buffers now for all secrets in ram 2025-07-15 08:32:33 +02:00
sneak c9774e89e0 WIP: refactor to use memguard for secure memory handling
- Add memguard dependency
- Update ReadPassphrase to return LockedBuffer
- Update EncryptWithPassphrase/DecryptWithPassphrase to accept LockedBuffer
- Remove string wrapper functions
- Update all callers to create LockedBuffers at entry points
- Update interfaces and mock implementations
2025-07-15 07:23:58 +02:00
sneak f9938135c6 fix: resolve all remaining linter issues (staticcheck, tagliatelle, lll)
- Fix staticcheck QF1011: Remove explicit type declaration for io.Writer variables
- Fix tagliatelle: Change all JSON tags from snake_case to camelCase
  - created_at → createdAt
  - keychain_item_name → keychainItemName
  - age_public_key → agePublicKey
  - age_priv_key_passphrase → agePrivKeyPassphrase
  - encrypted_longterm_key → encryptedLongtermKey
  - derivation_index → derivationIndex
  - public_key_hash → publicKeyHash
  - mnemonic_family_hash → mnemonicFamilyHash
  - gpg_key_id → gpgKeyId
- Fix lll: Break long function signature line to stay under 120 character limit

All linter issues have been resolved. The codebase now passes all linter checks.
2025-07-15 06:33:25 +02:00
sneak 386a27c0b6 fix: resolve all revive linter issues
Added missing package comments:
- cmd/secret/main.go
- internal/cli/cli.go
- internal/secret/constants.go
- internal/vault/management.go
- pkg/bip85/bip85.go

Fixed comment format issues for exported items:
- EnvStateDir, EnvMnemonic, EnvUnlockPassphrase, EnvGPGKeyID in constants.go
- Metadata, UnlockerMetadata, SecretMetadata, Configuration in metadata.go
- AppBIP39, AppHDWIF, AppXPRV in bip85.go

Replaced unused parameters with underscore (_):
- generate.go:39 - parameter 'args'
- init.go:30 - parameter 'args'
- unlockers.go:39,77,102 - parameter 'args' or 'cmd'
- vault.go:37 - parameter 'args'
- management.go:34 - parameter 'target'
2025-07-15 06:06:48 +02:00
sneak 080a3dc253 fix: resolve all nlreturn linter errors
Add blank lines before return statements in all files to satisfy
the nlreturn linter. This improves code readability by providing
visual separation before return statements.

Changes made across 24 files:
- internal/cli/*.go
- internal/secret/*.go
- internal/vault/*.go
- pkg/agehd/agehd.go
- pkg/bip85/bip85.go

All 143 nlreturn issues have been resolved.
2025-07-15 06:00:32 +02:00
sneak 811ddee3b7 fix: resolve all nestif linter errors
- Extract getLongTermPrivateKey helper function to reduce nesting in keychainunlocker.go and pgpunlocker.go
- Add getPassphrase helper method to reduce nesting in passphraseunlocker.go
- Refactor version serial extraction to use early returns in version.go
- Extract resolveRelativeSymlink and tryResolveOsSymlink helpers in management.go
- Add processMnemonicForVault helper to reduce nesting in vault creation
- Extract resolveUnlockerDirectory and readUnlockerPathFromFile helpers in unlockers.go
- Add findUnlockerByID helper to reduce duplicate code in RemoveUnlocker and SelectUnlocker

All tests pass after refactoring.
2025-07-15 05:47:16 +02:00
sneak 533133486c fix: remove unnecessary type conversions (unconvert)
- Remove unnecessary conversions of UnlockerMetadata in vault/unlockers.go
- The metadata variable is already of the correct type due to type aliasing
2025-06-20 12:52:19 -07:00
sneak be1f323a09 fix: remove unnecessary zero value initialization (revive)
- Remove explicit = 0 from uint32 declaration as it's the default zero value
2025-06-20 12:47:58 -07:00
sneak bdcddadf90 fix: resolve exported type stuttering issues (revive)
- Rename VaultMetadata to Metadata in internal/vault package to avoid stuttering
- Rename BIP85DRNG to DRNG in pkg/bip85 package to avoid stuttering
- Update all references in code and tests
2025-06-20 12:47:06 -07:00
sneak 08a42b16dd fix: replace os.Setenv with t.Setenv in tests (usetesting)
Replace os.Setenv calls with t.Setenv in test functions to ensure
proper test environment cleanup and better test isolation.
2025-06-20 09:13:01 -07:00
sneak 9e74b34b5d Fix remaining usetesting errors in vault integration test
Replace os.MkdirTemp() with t.TempDir() and os.Setenv() with t.Setenv()
Remove manual environment cleanup as t.Setenv handles it automatically
2025-06-20 08:58:29 -07:00
sneak e5b18202f3 Fix revive package stuttering errors
- Rename SecretMetadata to Metadata in secret package
- Rename SecretVersion to Version in secret package
- Update NewSecretVersion to NewVersion function
- Update all references across the codebase including:
  - vault package aliases
  - CLI usage
  - test files
  - method receivers and signatures
2025-06-20 08:48:17 -07:00
sneak 434b73d834 Fix intrange and G101 linting issues
- Convert for loops to use Go 1.22+ integer ranges in generate.go and helpers.go
- Disable G101 false positives for test vectors and environment variable names
- Add file-level gosec disable for bip85_test.go containing BIP85 test vectors
- Add targeted nolint comments for legitimate test data and constants
2025-06-20 08:08:01 -07:00
sneak 004dce5472 passes tests now! 2025-06-20 07:24:48 -07:00
sneak 0b31fba663 latest from ai, it broke the tests 2025-06-20 05:40:20 -07:00
sneak 03e0ee2f95 refactor: remove confusing dual ID method pattern from Unlocker interface - Removed redundant ID() method from Unlocker interface - Removed ID field from UnlockerMetadata struct - Modified GetID() to generate IDs dynamically based on unlocker type and data - Updated vault package to create unlocker instances when searching by ID - Fixed all tests and CLI code to remove ID field references - IDs are now consistently generated from unlocker data, preventing redundancy 2025-06-11 15:21:20 -07:00
sneak 9adf0c0803 refactor: fix redundant metadata fields across the codebase - Removed VaultMetadata.Name (redundant with directory structure) - Removed SecretMetadata.Name (redundant with Secret.Name field) - Removed AgePublicKey and AgeRecipient from PGPUnlockerMetadata - Removed AgePublicKey from KeychainUnlockerMetadata - Changed PGP and Keychain unlockers to store recipient in pub.txt instead of pub.age - Fixed all tests to reflect these changes - Follows DRY principle and prevents data inconsistency 2025-06-09 17:44:10 -07:00
sneak 1f89fce21b latest 2025-06-09 05:59:26 -07:00
sneak 02be4b2a55 Fix integration tests: correct vault derivation index and debug test failures 2025-06-09 04:54:45 -07:00
sneak d76a4cbf4d fix tests 2025-06-08 22:13:22 -07:00
sneak fbda2d91af add secret versioning support 2025-06-08 22:07:19 -07:00