clap alias makes both `relicario gen -l 32` and the long-form
`relicario generate -l 32` route to the same handler. The short flags
-l (length) and -w (words) were missing -- the READMEs existing example
`relicario generate -l 32` only actually worked after this commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 commits from feature/cli-tail-stream-b-session-manifest:
- 2e41e0b refactor(cli): single canonical ParamsFile in session.rs (Phase 5)
- 7901c27 refactor(cli): Vault::after_manifest_change wrapper (Phase 4)
- 4b657e7 refactor(cli): batched purge in cmd_purge and cmd_trash_empty (Phase 6)
- c4777cc refactor(cli): apply simplify findings (Phases 4-6 polish)
Phase 4 complete: Vault::after_manifest_change wrapper funnels NINE manifest-
mutation sites (not 7 as the spec/notes flagged -- attach.rs add+detach,
import.rs LastPass, and trash.rs cmd_trash_empty all previously SKIPPED
refresh_groups_cache; the wrapper now refreshes them as a side-effect).
save_manifest was DROPPED entirely (rather than just demoted to pub(crate)
as the spec said) -- the simplify pass found no escape hatch was needed,
so the only path to write the manifest now goes through the wrapper.
Stronger than spec.
Phase 5 complete: single pub(crate) struct ParamsFile in session.rs at
module level with Serialize+Deserialize. Constructors for_new_vault and
to_kdf_params (simplify pass changed into_kdf_params(self) to
to_kdf_params(&self) for ergonomics). commands/init.rs uses
ParamsFile::for_new_vault. On-disk JSON schema verified BYTE-STABLE via
fixture-string round-trip test (session::tests::params_file_round_trips_current_layout
+ for_new_vault_produces_expected_shape) -- same fields, same ordering,
same rename_all placement. Existing vaults read with no migration.
Phase 6 complete: purge_item renamed purge_item_filesystem, mutates only
filesystem + manifest, returns Vec<String> of paths. cmd_purge and
cmd_trash_empty both follow after_manifest_change -> git_rm -> git add ->
git commit. New helpers::git_rm extracted to DRY the pattern. Strict
invariant locked: tests/basic_flows.rs::trash_empty_batches_into_one_commit
counts commits via git rev-list --count HEAD before/after and asserts
delta == 1. A 50-item trash empty now fires 3 git invocations, not 52.
Simplify polish (c4777cc): all 5 findings legitimate, none rationale-skipped:
- Dropped redundant save_manifest_raw escape hatch
- Value-vs-self ergonomic fix (to_kdf_params(&self))
- DRY git_rm helper
- TOCTOU pre-check dropped from purge_item_filesystem
- Comment trim
3-way merge with stream-a (3dd1e1b) and stream-c (e69b347) clean: git
auto-resolved commands/add.rs (stream-a prompt_or_flag changes interleaved
with stream-b after_manifest_change call at the manifest-mutation site).
Verified semantic correctness via post-merge cargo test.
Pre-merge checklist on tip c4777cc + post-merge verification:
- cargo test --workspace standalone: 260 tests, 0 failures
- cargo test --workspace post-merge: 281 tests, 0 failures
- cargo clippy --workspace --all-targets -- -D warnings: silent
- cargo build -p relicario-wasm --target wasm32-unknown-unknown: clean
- Independent fresh-subagent code review: APPROVE
- grep refresh_groups_cache crates/relicario-cli/src/: zero matches
outside session.rs/helpers.rs (per spec done-criteria)
- grep struct ParamsFile crates/relicario-cli/src/: ONE match
(per spec done-criteria)
Plan B COMPLETE. With Phase 3 (Stream A) merged at 3dd1e1b and Phases 7+8
(Stream C) merged at e69b347, all eight Plan B phases are now on main.
One nit deferred (per subagent review): trash empty partial-failure
recovery -- if git_rm fails after after_manifest_change succeeds,
manifest.enc is rewritten in-tree and items are removed from disk but
no commit is made. Pre-existing behavior was strictly worse (per-item
interleaved partial-commit risk); current state is a net improvement.
Tree-cleanup-on-failure belongs in a follow-up plan, not this PR.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- session.rs: drop save_manifest_raw — its only caller was
after_manifest_change itself; the pub(crate) advertised the exact
bypass-the-cache-refresh footgun the wrapper exists to eliminate.
Inline the encrypt + atomic_write pair.
- session.rs: into_kdf_params(self) → to_kdf_params(&self). Body just
copies three u32s; the consume-self had no ownership benefit and
forced the round-trip test to rebuild a ParamsFile field-by-field.
- helpers.rs: add git_rm(repo, paths, context) wrapper around git_run
+ the load-bearing --ignore-unmatch flag. Replaces two near-identical
three-line "build rm_args, extend, git_run" blocks in trash.rs.
- trash.rs: purge_item_filesystem drops the if x.exists() pre-checks
(TOCTOU + redundant stat per item per trash-empty iteration). Uses
ErrorKind::NotFound swallow on remove_file/remove_dir_all instead.
- basic_flows.rs: trim trash_empty_batches_into_one_commit's sleep
comment to just the WHY.
3 commits from feature/cli-tail-stream-c-core-wasm-seam:
- e5d63ab refactor(core): extract base32 module, dedupe two RFC 4648 impls
- 03f2a1b refactor(core,cli): migrate CLI parsers to relicario-core, parse.rs becomes shim
- fc9264e feat(wasm): add parse_month_year, base32_decode_lenient, guess_mime exports
Phase 7 complete: parser bodies (MonthYear::parse, mime::guess_for_extension,
TotpConfig::parse_secret) lifted into relicario-core; CLI parse.rs reduced to
19-line thin shims; callsites unchanged. base32 codec deduplicated from two
inline implementations (item.rs encoder + import_lastpass.rs decoder) into
crate::base32::{encode_rfc4648, decode_rfc4648_lenient}. Steam Guards
non-RFC-4648 alphabet stays at item_types/totp.rs:13 with a neighbour
comment cross-referencing the standard module.
Phase 8 complete: 3 #[wasm_bindgen] exports (parse_month_year,
base32_decode_lenient, guess_mime) with snake_case JS names per existing
convention. extension/src/wasm.d.ts mirror landed in the same commit
(fc9264e) per kickoff hard-rule.
Spec deviation (PM ack 02:55Z + 15:13Z): pub(crate) mod base32 promoted to
pub mod base32 because the CLI shim AND the Phase 8 WASM exports both
require external reach. Justification documented in lib.rs module-list
comment + module-level docstring on base32.rs explicitly carving Steam
Guard out as a non-user.
Two new RelicarioError variants added (additive, non-breaking):
- InvalidBase32(String)
- InvalidMonthYear(String)
3-way merge with stream-a (3dd1e1b) clean: stream-c didn't actually modify
add.rs or prompt.rs, so the diff stat showing those files was just stream-c
being behind on stream-a's changes. ort strategy auto-took mains versions.
Pre-merge checklist on tip fc9264e + post-merge verification:
- cargo test --workspace standalone: 272 tests, 0 failures
- cargo test --workspace post-merge: 277 tests, 0 failures (5 added from stream-a)
- cargo clippy --workspace --all-targets: silent (both standalone + post-merge)
- cargo build -p relicario-wasm --target wasm32-unknown-unknown: clean
- Extension vitest: 17 failed / 335 passed -- matches cycle-1 baseline cluster, no new regressions
- Independent fresh-subagent code review: APPROVE-WITH-NITS
- nit 1: stale doc-comment in extension/src/shared/base32.ts:3 (Plan C concern, deferred)
- nit 2: TotpConfig::parse_secret unused on this branch (spec-driven forward-compat for Plan C SW handlers)
Plan B Phases 7+8 complete. With Phase 3 (Stream A) already merged at 3dd1e1b,
only Phases 4+5+6 (Stream B in flight) remain to close out Plan B.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renames purge_item to purge_item_filesystem — body becomes filesystem-only
(remove item.enc, remove attachments/<id>/, manifest.remove). Returns the
relative paths it removed. cmd_purge and cmd_trash_empty accumulate the
paths and fire ONE git rm + ONE git add + ONE git commit per invocation.
A 50-item trash empty now produces 3 git subprocesses regardless of N
(was N+2). New regression test trash_empty_batches_into_one_commit asserts
the one-commit invariant via git rev-list --count.
Plan B Phase 8 — three #[wasm_bindgen] exports for the parsers migrated
in Phase 7, mirrored in extension/src/wasm.d.ts under "Pure parsers
(no session needed)". snake_case JS naming consistent with every
existing export; SessionHandle not required.
- parse_month_year(s) → { month, year } via js_value_for
- base32_decode_lenient(s) → Uint8Array
- guess_mime(filename) → string
Tests in session_tests mod cover the OK paths; error-path / JsValue
serialization can't be tested natively (JsError construction panics
off-wasm) and is covered in core (time::tests + base32::tests).
Plan C will wire SW message handlers consuming these exports in a
future round; this commit delivers only the seam.
Includes simplify-feedback fixes:
- relicario-core lib.rs module-list mentions base32 and mime
- item_types/totp.rs neighbour comment unified to ///-style block
cargo test --workspace: green
cargo clippy --workspace: silent
cargo build -p relicario-wasm --target wasm32-unknown-unknown: clean
cd extension && npm run test: 17 pre-existing failures only (baseline)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the canonical post-mutation funnel: save_manifest_raw + groups.cache
refresh in one method. Converts nine commands/*.rs mutation callsites from
the manual save_manifest + refresh_groups_cache pair to a single
vault.after_manifest_change(&manifest)?. save_manifest renamed to
save_manifest_raw (pub(crate)) so future commands cannot accidentally
bypass the cache refresh. Four of the nine sites (attach.rs add/detach,
import.rs LastPass, trash.rs cmd_trash_empty's per-item save) previously
skipped the cache refresh — the wrapper fixes them. refresh_groups_cache
moves from main.rs to helpers.rs so the read-side warmup callers in
get.rs/list.rs still reach it.
Plan B Phase 3 sub-step 2. Replaces the
title.map(Ok).unwrap_or_else(|| prompt("Title"))? chain in all
seven build_*_item functions with prompt_or_flag, and folds
login's or_else(|| prompt_optional(...).ok().flatten()) for
username and url into prompt_or_flag_optional. prompt_secret
sites and the parse-on-Some-only patterns (expiry, dob, card
kind, totp algorithm) stay as-is per spec. Removes the
#[allow(dead_code)] attributes from the four helpers in
prompt.rs now that callers exist.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Promotes ParamsFile to a module-level pub(crate) struct with both Serialize
and Deserialize derives. for_new_vault() constructor + into_kdf_params()
inversion replace the two-definition split between commands/init.rs (write)
and session.rs read_params (read). On-disk JSON format unchanged — fixture
test asserts round-trip with the current params.json layout.
Plan B Phase 7 sub-step 2 — moves the bodies of parse_month_year,
base32_decode_lenient, guess_mime from crates/relicario-cli/src/parse.rs
to relicario-core. The CLI's parse.rs becomes a 19-line shim re-exporting
the new core API.
New core surface:
- time::MonthYear::parse (Result<_, RelicarioError>)
- mime::guess_for_extension (new mime module)
- item_types::TotpConfig::parse_secret — Zeroizing<Vec<u8>> wrapper
over base32::decode_rfc4648_lenient
base32 module promoted from pub(crate) to pub so non-core consumers
(CLI shim, future Phase 8 WASM exports) can reach it. New
RelicarioError::InvalidMonthYear(String) for the parse error path
(mirrors sub-step 1's InvalidBase32). MonthYear::new keeps its
&'static str error type — bringing it to RelicarioError is DEV-A's P3.
CLI callsites unchanged (commands/{add,edit,attach}.rs); RelicarioError
auto-converts to anyhow::Error at ? boundaries.
cargo test --workspace: green (core 143, +7 from new tests)
cargo clippy --workspace: silent
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plan B Phase 3 sub-step 1. The new helpers collapse the
Option<T>::map(Ok).unwrap_or_else(|| prompt(...))? chain that
the seven build_*_item builders repeat. Reader is injectable
via the *_with_reader variants so the unit tests can drive
both the flag-value and prompt paths from a Cursor without
needing a TTY. prompt and prompt_optional are refactored to
delegate to two private read_*_line helpers; semantics are
unchanged. dead_code is allowed on the four new helpers
until sub-step 2 wires them into commands/add.rs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New crates/relicario-core/src/base32.rs hosts encode_rfc4648 +
decode_rfc4648_lenient (case-insensitive, optional padding, whitespace
stripped). Folds inline base32_encode (item.rs:255-275) and
decode_base32_totp (import_lastpass.rs:202-220) into the shared module;
both call sites updated.
- New RelicarioError::InvalidBase32(String) variant for the decoder
error path
- Module is pub(crate); public API surface unchanged
- Steam alphabet (item_types/totp.rs:13) intentionally separate with
neighbour comment pointing at crate::base32
Plan B Phase 7 sub-step 1 (DEV-A P2 base32 dedup half).
docs/superpowers/specs/2026-05-04-cli-restructure-design.md.
cargo test --workspace: green
cargo clippy --workspace: silent
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 code-quality review caught that the [`RECOVERY_PRODUCTION_PARAMS`]
form in the module header introduced a new rustdoc warning (the const is
module-private, so the link only resolves under --document-private-items).
Drop the brackets so it renders as plain backticks — same visual, no
broken link, no need to widen visibility.
Refs: docs/superpowers/specs/2026-05-04-security-polish-design.md (Phase 3)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 of the security-polish series. Brings recovery_qr.rs up to
the documentation density of crypto.rs / imgsecret.rs / backup.rs /
tar_safe.rs. No runtime behaviour change: just module-level //! header
explaining the format + KDF domain separation + parameter-pinning
rationale, an ASCII diagram of the 109-byte payload layout pinned by
a static assertion, doc-comments on the four public items, and named
slice-range constants for the offset arithmetic.
production_params() is replaced with a top-level const so the "pinned,
do not change once shipped" property is visible at every use site.
Refs: docs/superpowers/specs/2026-05-04-security-polish-design.md (Phase 3)
Refs: docs/superpowers/reviews/2026-05-04-architecture-review.md (P1.7)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the P1.1 defense-in-depth gap: wasm-bindgen's auto-generated
.free() previously dropped the SessionHandle wrapper (a u32) without
removing the SESSIONS HashMap entry, leaving the master key and
image_secret in WASM linear memory until JS explicitly called
lock(handle). Drop now wires .free() to session::remove, and the
new native test pins the contract.
Refs: docs/superpowers/specs/2026-05-04-security-polish-design.md (Phase 1)
Refs: docs/superpowers/reviews/2026-05-04-architecture-review.md (P1.1)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catches the workspace and the extension manifests up to the v0.5.x
release line (was still showing 0.2.0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The groups-cache opt-out is a developer debugging knob, not a
user-facing config. Gating the env-var lookup behind cfg!(debug_assertions)
makes release builds ignore the variable; the optimiser removes the
lookup entirely, so the variable name doesn't appear in release binary
strings output.
Doc-comments updated to reflect the new behaviour.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
verify_commit previously loaded devices.json/revoked.json and threw
both away, accepting any commit whose stderr contained "GOODSIG" or
"Good signature". This left device registration and revocation as
no-ops: unregistered keys could push, revoked keys kept working.
The fix:
- Build a temp gpg.ssh.allowedSignersFile from devices.json at the
commit, passed via GIT_CONFIG_COUNT/KEY/VALUE env (no global git
config mutation).
- Run git verify-commit --raw and parse SHA256 fingerprint from stderr
regardless of exit code (SSH git outputs the "Good" line even for
keys not in allowed-signers, with "No principal matched" + exit 1).
- Check revoked.json FIRST: reject if committer_ts >= revoked_at;
accept historical commits (committer_ts < revoked_at).
- Reject if fingerprint is not in active devices.json.
- Bootstrap: accept only when BOTH devices.json AND revoked.json are
empty/absent (not just devices.json alone).
Acceptance: 4 integration tests covering the matrix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps ssh-key's PublicKey::fingerprint(HashAlg::Sha256). Output format
matches ssh-keygen -lf and git verify-commit --raw stderr
(SHA256:<43-char base64>). Used by the upcoming relicario-server
verify-commit rewrite (audit S1).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>