Compare commits
8 Commits
feature/v0
...
v0.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fa4d6824c | ||
|
|
783e3493f0 | ||
|
|
4cca9b465c | ||
|
|
5be3043ab5 | ||
|
|
cf89bf8ca4 | ||
|
|
a91ceea0ed | ||
|
|
415d8ed9ef | ||
|
|
c5b1917eb0 |
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,5 +1,56 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.8.1 — 2026-06-20 — org item-type parity + collection-scoped attachments
|
||||||
|
|
||||||
|
Brings `relicario org add` / `relicario org edit` to **full item-type parity** with the
|
||||||
|
personal vault: the org surface now supports **all 7 item types** (previously Login /
|
||||||
|
SecureNote / Identity only), adds collection-scoped attachment storage for Document
|
||||||
|
items, and grant-scopes attachment write paths in the pre-receive hook — closing a latent
|
||||||
|
authorization gap. Secrets are entered via interactive prompts by default, with `--*-stdin`
|
||||||
|
escape hatches for non-interactive scripting. Tracked under
|
||||||
|
`docs/superpowers/plans/2026-06-20-relicario-v0.8.1-parity.md`.
|
||||||
|
|
||||||
|
> **⚠️ Coordinated server redeploy required.** The `relicario-server` pre-receive hook
|
||||||
|
> (now `0.1.1`) must be rebuilt and redeployed for attachment writes to be grant-scoped in
|
||||||
|
> production. Until the updated hook is installed, `attachments/…` pushes remain
|
||||||
|
> `Unrestricted` (gated only by the per-commit member-signature check).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Shared `item_build` CLI module** (`crates/relicario-cli/src/commands/item_build.rs`):
|
||||||
|
centralizes per-type secret resolution, item builders (`build_*`), and interactive edit
|
||||||
|
helpers (`edit_*`) consumed by **both** the personal and org command surfaces, eliminating
|
||||||
|
the prior personal↔org builder duplication.
|
||||||
|
- **Org `add` / `edit` parity for Card, Key, TOTP, and Document** — `relicario org add` now
|
||||||
|
creates all 7 item types; `relicario org edit` is interactive per-type ("blank to keep",
|
||||||
|
field-history capture) instead of flat flags.
|
||||||
|
- **`--*-stdin` secret flags** on personal and org `add` for non-interactive entry of
|
||||||
|
passwords, card number/CVV/PIN, key material, TOTP secrets, and note bodies.
|
||||||
|
- **Collection-scoped org attachment storage** (`crates/relicario-cli/src/org_session.rs`):
|
||||||
|
attachments stored at `attachments/<slug>/<item-id>/<att-id>.enc` with a default
|
||||||
|
per-attachment cap (10 MiB, mirroring the personal default at
|
||||||
|
`crates/relicario-core/src/settings.rs`). `org add document --file`, `org edit --file`
|
||||||
|
(replace), and `org purge` (removes the item's attachment directory) round-trip with
|
||||||
|
git-status-clean staging.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **Grant-scoped attachment writes** (`relicario-server` `0.1.1`): `classify_path` now
|
||||||
|
recognizes `attachments/<slug>/<item-id>/<att-id>.enc` (exactly 3 path segments, `.`-free
|
||||||
|
slug guard) as `Item { collection }`, bringing attachment writes under the same grant +
|
||||||
|
slug-existence check as `items/` blobs. Previously such paths fell through to
|
||||||
|
`Unrestricted`. The Document source plaintext is read into a `Zeroizing` buffer and wiped
|
||||||
|
after encryption. See `docs/SECURITY.md`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Personal `add secure-note` `--body-prompt` flag renamed to `--body-stdin` (unified
|
||||||
|
multiline-secret model).
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
- Updated cli `ARCHITECTURE.md`, `docs/FORMATS.md` (org attachment layout + cap citation),
|
||||||
|
`docs/SECURITY.md`, `STATUS.md`, and `ROADMAP.md`. New
|
||||||
|
`docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md` is the forward
|
||||||
|
plan for extension↔CLI parity (org read/write plus a cluster of personal-side extension
|
||||||
|
gaps). End-user `user_docs/` guide lands as a fast-follow.
|
||||||
|
|
||||||
## v0.8.0 — 2026-06-20 — enterprise org vault
|
## v0.8.0 — 2026-06-20 — enterprise org vault
|
||||||
|
|
||||||
Git-native multi-user **org vaults**: a separate org git repository alongside each
|
Git-native multi-user **org vaults**: a separate org git repository alongside each
|
||||||
|
|||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2156,7 +2156,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "relicario-cli"
|
name = "relicario-cli"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arboard",
|
"arboard",
|
||||||
@@ -2188,7 +2188,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "relicario-core"
|
name = "relicario-core"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -2235,7 +2235,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "relicario-wasm"
|
name = "relicario-wasm"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ under `src/commands/`. Each source file has one job.
|
|||||||
- **`src/main.rs`** (`main.rs:1-492`) — clap surface and the flat dispatcher.
|
- **`src/main.rs`** (`main.rs:1-492`) — clap surface and the flat dispatcher.
|
||||||
Owns the top-level `Cli` / `Commands` enum and every subcommand enum
|
Owns the top-level `Cli` / `Commands` enum and every subcommand enum
|
||||||
(`AddKind`, `TrashAction`, `SettingsAction`, `BackupAction`, `ImportAction`,
|
(`AddKind`, `TrashAction`, `SettingsAction`, `BackupAction`, `ImportAction`,
|
||||||
`DeviceAction`, `RecoveryQrCmd`). `main()` is a single `match` that
|
`DeviceAction`, `RecoveryQrCmd`), plus the org clap surface `OrgCommands`
|
||||||
|
(`main.rs:448`) and `OrgAddKind` (`main.rs:556`) — the latter's Card / Key /
|
||||||
|
Document / Totp variants carry `--collection` and the `--*-stdin` secret flags.
|
||||||
|
`main()` is a single `match` that
|
||||||
delegates each variant to `commands::<verb>::cmd_<verb>(...)`. Also owns the
|
delegates each variant to `commands::<verb>::cmd_<verb>(...)`. Also owns the
|
||||||
three test-only env-var hooks (`test_passphrase_override`,
|
three test-only env-var hooks (`test_passphrase_override`,
|
||||||
`test_item_secret_override`, `test_backup_passphrase_override`) — each is
|
`test_item_secret_override`, `test_backup_passphrase_override`) — each is
|
||||||
@@ -94,7 +97,14 @@ under `src/commands/`. Each source file has one job.
|
|||||||
(`items/<collection-slug>/<id>.enc` — the leading slug is what the pre-receive
|
(`items/<collection-slug>/<id>.enc` — the leading slug is what the pre-receive
|
||||||
hook authorizes against, never decrypting), fingerprint-based member matching
|
hook authorizes against, never decrypting), fingerprint-based member matching
|
||||||
(`relicario_core::fingerprint`, tolerant of OpenSSH whitespace/comment
|
(`relicario_core::fingerprint`, tolerant of OpenSSH whitespace/comment
|
||||||
differences), `atomic_write`, and `org_git_run`. Note `org_git_run` runs
|
differences), `atomic_write`, and `org_git_run`. As of v0.8.1 it also owns
|
||||||
|
**collection-scoped attachment storage** — `attachment_path` /
|
||||||
|
`save_attachment` / `load_attachment` / `remove_item_attachments`
|
||||||
|
(`org_session.rs:125-157`) at layout
|
||||||
|
`attachments/<collection-slug>/<item-id>/<att-id>.enc` (the same leading slug
|
||||||
|
the pre-receive hook authorizes against as for `item_path`), capped
|
||||||
|
per-attachment by `DEFAULT_ORG_ATTACHMENT_MAX_BYTES` (10 MiB,
|
||||||
|
`org_session.rs:20`). Note `org_git_run` runs
|
||||||
**bare git** — unlike `helpers::git_run` it does NOT inject
|
**bare git** — unlike `helpers::git_run` it does NOT inject
|
||||||
`commit.gpgsign=false`, because org commits MUST be signed (the hook verifies
|
`commit.gpgsign=false`, because org commits MUST be signed (the hook verifies
|
||||||
every commit's signature); signing config is established by
|
every commit's signature); signing config is established by
|
||||||
@@ -111,19 +121,38 @@ under `src/commands/`. Each source file has one job.
|
|||||||
concurrent-rotation abort), `transfer-ownership`, `delete-org`, `status` /
|
concurrent-rotation abort), `transfer-ownership`, `delete-org`, `status` /
|
||||||
`audit` (verified-signer attribution + `TAMPERED` flag).
|
`audit` (verified-signer attribution + `TAMPERED` flag).
|
||||||
|
|
||||||
*Item CRUD (7):* `org add` creates typed items via `OrgAddKind`
|
*Item CRUD (7):* full item-type parity with the personal vault (v0.8.1).
|
||||||
(`commands/org.rs:749`) — **Login / SecureNote / Identity only**; Card /
|
`org add` creates **all seven types** (Login / SecureNote / Identity / Card /
|
||||||
SshKey / Document / Totp creation is a deferred follow-up. `get` / `list` can
|
Key / Document / Totp) via `OrgAddKind` (`commands/org.rs:751`); each arm
|
||||||
display any item type if present. `org get <query> [--show]` masks secrets
|
delegates to the shared `item_build::build_*` builders through `build_org_item`
|
||||||
unless `--show`; `org list [--trashed]` filters by the caller's collection
|
(`commands/org.rs:799`), and `run_add` (`commands/org.rs:823`) sets tags
|
||||||
grants; `org edit <query>` is flag-driven (blank flags keep current values);
|
post-build. Document is special-cased in `run_add` (`commands/org.rs:839`): its
|
||||||
`org rm` soft-deletes, `org restore` undoes, `org purge` permanently removes
|
builder also yields an `EncryptedAttachment` that is written via
|
||||||
the encrypted blob. All item ops are collection-scoped and grant-enforced. The
|
`save_attachment` and git-staged before the signed commit. Single-line secrets
|
||||||
audit trail emits `item-create` / `item-update` / `item-delete` /
|
(card number/CVV/PIN, TOTP secret, login password) accept a `--*-stdin` flag;
|
||||||
`item-restore` / `item-purge`.
|
multiline secrets (Key material, SecureNote body) read stdin to EOF — the same
|
||||||
|
`resolve_secret_line` / `resolve_secret_multiline` convention as personal `add`
|
||||||
|
(`commands/item_build.rs`).
|
||||||
|
|
||||||
Deferred: Card / SshKey / Document / Totp `org add` / `edit` parity;
|
`org edit <query>` (`run_edit`, `commands/org.rs:1004`) is **interactive
|
||||||
extension org reads and writes (Dev-D).
|
per-type** as of v0.8.1 (it was flag-driven before): it prompts Title, then
|
||||||
|
dispatches on `&mut item.core` to the shared `item_build::edit_*` helpers
|
||||||
|
("blank keeps current", field-history capture via `push_history`), mirroring
|
||||||
|
personal `cmd_edit`. `--totp-qr` sets a Login TOTP from a QR image; `--file`
|
||||||
|
replaces a Document's primary attachment (`commands/org.rs:1039`, rejected for
|
||||||
|
non-Document items at `commands/org.rs:1018`). The edit commit carries
|
||||||
|
`Relicario-Action: item-update`.
|
||||||
|
|
||||||
|
`org get <query> [--show]` masks every secret unless `--show`; `org list
|
||||||
|
[--trashed]` filters by the caller's collection grants; `org rm` soft-deletes,
|
||||||
|
`org restore` undoes, `org purge` (`run_purge`, `commands/org.rs:1164`)
|
||||||
|
permanently removes the encrypted blob **and** the item's attachment directory
|
||||||
|
(`remove_item_attachments`, `commands/org.rs:1173`). All item ops are
|
||||||
|
collection-scoped and grant-enforced (`filter_for_member` over the manifest +
|
||||||
|
`ensure_grant` before any load/mutate). The audit trail emits `item-create` /
|
||||||
|
`item-update` / `item-delete` / `item-restore` / `item-purge`.
|
||||||
|
|
||||||
|
Deferred: extension org reads and writes (Plan B-2 / phase 2).
|
||||||
|
|
||||||
- **`src/helpers.rs`** (`helpers.rs:1-101`) — pure, no-state plumbing:
|
- **`src/helpers.rs`** (`helpers.rs:1-101`) — pure, no-state plumbing:
|
||||||
`find_vault_dir_from` (`helpers.rs:14-28`) walks up parent directories
|
`find_vault_dir_from` (`helpers.rs:14-28`) walks up parent directories
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "relicario-cli"
|
name = "relicario-cli"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "CLI for relicario password manager"
|
description = "CLI for relicario password manager"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting.
|
//! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
@@ -255,23 +255,39 @@ pub(crate) fn build_totp(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a file and encrypt it as an attachment under `key`, deriving its display
|
||||||
|
/// metadata. The plaintext is held in a `Zeroizing` buffer so it is wiped after
|
||||||
|
/// encryption. Returns the encrypted blob plus (filename, mime_type, size).
|
||||||
|
pub(crate) fn encrypt_document_file(
|
||||||
|
path: &Path,
|
||||||
|
key: &Zeroizing<[u8; 32]>,
|
||||||
|
max_bytes: u64,
|
||||||
|
) -> Result<(EncryptedAttachment, String, String, u64)> {
|
||||||
|
use relicario_core::encrypt_attachment;
|
||||||
|
let bytes = Zeroizing::new(
|
||||||
|
std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?,
|
||||||
|
);
|
||||||
|
let enc = encrypt_attachment(&bytes, key, max_bytes)?;
|
||||||
|
let filename = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", path.display()))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
let mime_type = crate::parse::guess_mime(&filename);
|
||||||
|
Ok((enc, filename, mime_type, bytes.len() as u64))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn build_document(
|
pub(crate) fn build_document(
|
||||||
title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64,
|
title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64,
|
||||||
) -> Result<(Item, EncryptedAttachment)> {
|
) -> Result<(Item, EncryptedAttachment)> {
|
||||||
use relicario_core::item_types::DocumentCore;
|
use relicario_core::item_types::DocumentCore;
|
||||||
use relicario_core::{encrypt_attachment, AttachmentRef};
|
use relicario_core::AttachmentRef;
|
||||||
let bytes = std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
|
let (enc, filename, mime_type, size) = encrypt_document_file(&file, key, max_bytes)?;
|
||||||
let enc = encrypt_attachment(&bytes, key, max_bytes)?;
|
|
||||||
let filename = file.file_name()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", file.display()))?
|
|
||||||
.to_string_lossy().into_owned();
|
|
||||||
let mime_type = crate::parse::guess_mime(&filename);
|
|
||||||
let primary_attachment = enc.id.clone();
|
|
||||||
let mut item = Item::new(title, ItemCore::Document(DocumentCore {
|
let mut item = Item::new(title, ItemCore::Document(DocumentCore {
|
||||||
filename: filename.clone(), mime_type: mime_type.clone(), primary_attachment: primary_attachment.clone(),
|
filename: filename.clone(), mime_type: mime_type.clone(), primary_attachment: enc.id.clone(),
|
||||||
}));
|
}));
|
||||||
item.attachments.push(AttachmentRef {
|
item.attachments.push(AttachmentRef {
|
||||||
id: primary_attachment, filename, mime_type, size: bytes.len() as u64, created: item.created,
|
id: enc.id.clone(), filename, mime_type, size, created: item.created,
|
||||||
});
|
});
|
||||||
Ok((item, enc))
|
Ok((item, enc))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1038,25 +1038,18 @@ pub fn run_edit(dir: &Path, query: &str, totp_qr: Option<std::path::PathBuf>, fi
|
|||||||
ItemCore::Key(k) => ib::edit_key(k, history)?,
|
ItemCore::Key(k) => ib::edit_key(k, history)?,
|
||||||
ItemCore::Document(d) => {
|
ItemCore::Document(d) => {
|
||||||
if let Some(path) = &file {
|
if let Some(path) = &file {
|
||||||
let bytes = std::fs::read(path)
|
let (enc, filename, mime_type, size) = ib::encrypt_document_file(
|
||||||
.with_context(|| format!("read {}", path.display()))?;
|
path, vault.key(), crate::org_session::DEFAULT_ORG_ATTACHMENT_MAX_BYTES)?;
|
||||||
let enc = relicario_core::encrypt_attachment(
|
|
||||||
&bytes, vault.key(), crate::org_session::DEFAULT_ORG_ATTACHMENT_MAX_BYTES)?;
|
|
||||||
vault.remove_item_attachments(&collection, &id)?;
|
vault.remove_item_attachments(&collection, &id)?;
|
||||||
let rel = vault.save_attachment(&collection, &id, &enc)?;
|
let rel = vault.save_attachment(&collection, &id, &enc)?;
|
||||||
let filename = path
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", path.display()))?
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned();
|
|
||||||
d.mime_type = crate::parse::guess_mime(&filename);
|
|
||||||
d.primary_attachment = enc.id.clone();
|
|
||||||
d.filename = filename.clone();
|
d.filename = filename.clone();
|
||||||
|
d.mime_type = mime_type.clone();
|
||||||
|
d.primary_attachment = enc.id.clone();
|
||||||
new_doc_attachments = Some(vec![relicario_core::AttachmentRef {
|
new_doc_attachments = Some(vec![relicario_core::AttachmentRef {
|
||||||
id: enc.id,
|
id: enc.id,
|
||||||
filename,
|
filename,
|
||||||
mime_type: d.mime_type.clone(),
|
mime_type,
|
||||||
size: bytes.len() as u64,
|
size,
|
||||||
created: now_unix(),
|
created: now_unix(),
|
||||||
}]);
|
}]);
|
||||||
doc_attachment_rel = Some(rel);
|
doc_attachment_rel = Some(rel);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "relicario-core"
|
name = "relicario-core"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Core library for relicario password manager"
|
description = "Core library for relicario password manager"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "relicario-wasm"
|
name = "relicario-wasm"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "WASM bindings for relicario password manager"
|
description = "WASM bindings for relicario password manager"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Extension ↔ CLI Parity Gap Analysis
|
||||||
|
|
||||||
|
- **Date:** 2026-06-20
|
||||||
|
- **Author:** Dev-D, reconciled against an independent PM parity sweep
|
||||||
|
- **Status:** Draft for review — **forward-planning**, NOT v0.8.1 scope
|
||||||
|
- **Anchor commit:** `origin/main` `b09e0ce` (v0.8.0 org vault + v0.8.1 Dev-A foundation merged; Dev-B/C/D in flight)
|
||||||
|
- **Scope note:** This plans a *future* milestone. Extension org **writes** remain explicitly out of scope for v0.8.1 per `docs/superpowers/specs/2026-06-20-relicario-v0.8.1-parity.md` (Plan B-2).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Survey the gap between the Relicario **CLI** (`relicario`) and the **browser extension**, classify every gap as a *real parity gap*, an *intended CLI-only* capability, or *already-planned-in-a-spec*, and produce a prioritized work list (with rough sizing) to bring the extension up to CLI parity. The driver is the project's **CLI/extension parity philosophy**: features should not ship "CLI-first, extension-later" without an explicit, recorded decision — this doc is that record for the current backlog.
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
Two **independent** surveys were run and then reconciled:
|
||||||
|
|
||||||
|
- **PM sweep** — 3 inventory agents + synthesis.
|
||||||
|
- **Dev-D sweep** — 4 parallel readers (CLI / extension-UI / extension-SW / specs+roadmap) → synthesis → an adversarial completeness/accuracy critic, all reading from a worktree pinned at `b09e0ce`.
|
||||||
|
|
||||||
|
The two sweeps were deliberately blind to each other. All load-bearing claims in this document were **hand-verified against source** (greps + line reads); where the two sweeps disagree, the disagreement is flagged explicitly in §Reconciliation. Line citations are point-in-time against `b09e0ce` and may drift.
|
||||||
|
|
||||||
|
## Executive summary
|
||||||
|
|
||||||
|
Core item-CRUD parity is **excellent**. All 7 item types (Login, Secure Note, Identity, Card, Key, TOTP, Document) and the add / edit / view / list / trash / restore lifecycle are at full parity, and in several places the **extension is the richer surface** (live TOTP codes, custom fields/sections, TOTP-from-QR, password coloring, session auto-lock, autofill/capture). Where a *per-type* gap exists it is most often on the **CLI** side, not the extension's.
|
||||||
|
|
||||||
|
The genuine **extension-side** gaps cluster into three buckets:
|
||||||
|
|
||||||
|
1. **Metadata-management gaps (the headline finding):** editing **groups**, **tags**, and **filtering** is wired into only specific forms/surfaces in the extension, while the CLI offers them uniformly across all types; **favorites** has *zero* extension UI (strictly worse than the CLI). These are real, currently-shipping parity gaps on the *personal* vault.
|
||||||
|
2. **Backend-exists-but-no-wire/UI:** attachment **removal** (`removeAttachmentsFromItem` helper exists, no `remove_attachment` router message), **per-item purge** (`purge_item` handler exists, only a bulk "empty trash" UI), and the `isInTab()` popup-mode gate that hides login/secure-note attachment editing in the popup window.
|
||||||
|
3. **The org (enterprise) vault** — the single largest gap. The entire org feature (shipped CLI-only in v0.8.0) has **no extension presence** (no org routes, no org context). This is fully specced and explicitly deferred (Dev-D org-read / Plan B-2 org-write).
|
||||||
|
|
||||||
|
Plus one quality gap on the personal side surfaced by the PM sweep: **autofill hostname matching** is a naive exact-equality match.
|
||||||
|
|
||||||
|
Intentionally **CLI-only by design** (not gaps): real `git pull`/`push` and `.git`-history backup bundling (the extension writes straight to the host Contents API and keeps no local repo), the `imgsecret embed` recovery subcommand, recovery-QR's deliberate no-file-write contract, org **admin** (members/collections/grants/rotate/audit), and shell completions.
|
||||||
|
|
||||||
|
## Reconciliation with the PM sweep
|
||||||
|
|
||||||
|
The PM sweep concluded: extension at *near-full parity* on the personal surface, ahead in places, with the **org vault as the one material gap** and **autofill hostname matching as the only personal-side quality gap**.
|
||||||
|
|
||||||
|
**Agreements (both sweeps, independently):**
|
||||||
|
- Org vault is the largest gap; it is fully specced and deferred (Dev-D read / Plan B-2 write).
|
||||||
|
- The extension leads on live TOTP, custom fields/sections, password coloring.
|
||||||
|
- The intended-CLI-only set: git sync/push, `.git` backup bundling, device-key deploy-key plumbing, org admin, shell completions.
|
||||||
|
|
||||||
|
**Dev-D refines / partially refutes the PM:** the personal surface is **not** "near-full parity with autofill as the only gap." There is a real cluster of **personal-side extension gaps** the PM sweep understated:
|
||||||
|
- **Favorites — none in the extension** (`favorite` only round-trips through save fns; no toggle, no star in lists, no filter). The CLI is itself only add-only, so the extension is *strictly worse*. The PM hypothesis did not list this.
|
||||||
|
- **Group editing — Login-form only** (`f-group` + `wireGroupAutocomplete` live in `login.ts` only; card/key/identity/totp/document forms pass `group` through without an input).
|
||||||
|
- **Tag editing — Document-form only** (`f-tags` in `document.ts` only; other forms preserve-but-don't-edit).
|
||||||
|
- **Filter — popup has no type filter** (vault-tab only) and **no tag filter** anywhere.
|
||||||
|
- **Per-item purge** and **attachment add/remove** have working backends but no popup-reachable UI / no router wire.
|
||||||
|
|
||||||
|
**PM caught, Dev-D's taxonomy missed:** **autofill hostname matching.** `service-worker/vault.ts` (`findByHostname`, equality at `:344`) matches credentials by exact `icon_hint` equality (`(e.icon_hint ?? '').toLowerCase() === hostname`) — no `www.` strip, no registrable-domain (eTLD+1) match, so `www.example.com` will not match an item stored as `example.com`. Confirmed; folded in as a real LOW-MED personal-side gap. (Dev-D's capability taxonomy centered on item-CRUD/features and under-weighted the content-script autofill path — the PM sweep is the reason it appears here.)
|
||||||
|
|
||||||
|
**Methodology correction (a Dev-D self-sweep error, struck here):** the Dev-D extension-SW inventory referred to a `messages.ts` "that does not exist at that path." **That is false** — the file exists at `extension/src/shared/messages.ts` (227 lines): it holds the `PopupMessage` union (with `delete_item // soft-delete` at line 23), `POPUP_ONLY_TYPES` (line 168), and `CONTENT_CALLABLE_TYPES` (line 224). The inventory had merely dropped the `shared/` directory prefix. The substantive findings it supported (the unwired `searchItems`/`removeAttachmentsFromItem` helpers) are independently verified correct; the "file doesn't exist" caveat is removed from this document.
|
||||||
|
|
||||||
|
## Parity matrix
|
||||||
|
|
||||||
|
Support: **full** / **partial** / **none** / **n/a**. `gap_class`: **at-parity** · **real-gap** (extension work) · **real-gap (CLI-side)** (extension already ahead; CLI backlog) · **cli-only-by-design** · **already-planned**.
|
||||||
|
|
||||||
|
### Item types
|
||||||
|
|
||||||
|
| Capability | CLI | Ext | gap_class | Notes (evidence @ `b09e0ce`) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Login: create/view/edit | full | full | at-parity | CLI `add/get/edit login`; ext form + `add_item`/`update_item`/`get_item`. |
|
||||||
|
| Secure Note: create/view/edit | full | full | at-parity | Both complete. |
|
||||||
|
| Identity: create | full | full | at-parity | Both; ext also exposes `address`. |
|
||||||
|
| Identity: view | full | full | at-parity | Both. |
|
||||||
|
| Identity: edit | partial | full | real-gap (CLI-side) | CLI `edit_identity` omits `date_of_birth` + records no history; ext edits all. CLI backlog. |
|
||||||
|
| Card: create/view | full | full | at-parity | Both. |
|
||||||
|
| Card: edit | partial | full | real-gap (CLI-side) | CLI `edit_card` = holder+number only (no CVV/PIN/expiry/kind); ext edits all. CLI backlog. |
|
||||||
|
| Key: create/view | full | full | at-parity | Both; ext takes `public_key` interactively. |
|
||||||
|
| Key: edit | partial | full | real-gap (CLI-side) | CLI `edit_key` = key-material only (no label/algorithm/public_key); ext edits all. CLI backlog. |
|
||||||
|
| TOTP: create | full | full | at-parity | Both; ext adds Steam Guard kind. |
|
||||||
|
| TOTP: view | partial | full | real-gap (CLI-side) | CLI shows metadata only; ext shows live rotating code. See "TOTP live code". |
|
||||||
|
| TOTP: edit | full | full | at-parity | Both. |
|
||||||
|
| Document: create | full | full | at-parity | CLI encrypts file as attachment; ext `upload_attachment`. |
|
||||||
|
| Document: view | partial | full | real-gap (CLI-side) | CLI metadata + `extract`; ext inline image preview. CLI backlog. |
|
||||||
|
| Document: edit | none | full | real-gap (CLI-side) | CLI `edit` on Document is a no-op redirect to attach/extract; ext changes primary/supplementary files. CLI backlog. |
|
||||||
|
|
||||||
|
### Operations
|
||||||
|
|
||||||
|
| Capability | CLI | Ext | gap_class | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| add / edit / get / list | full | full | at-parity | All 7 types both surfaces. |
|
||||||
|
| rm / soft-delete | full | full | at-parity | CLI `rm`; ext `delete_item` (`messages.ts:23`, handler `popup-only.ts`). |
|
||||||
|
| trash (list) | full | full | at-parity | CLI `trash`; ext trash view. |
|
||||||
|
| restore from trash | full | full | at-parity | CLI `restore`; ext `restore_item`. |
|
||||||
|
| purge (permanent) | full | partial | **real-gap** | Ext UI only bulk "empty trash" (`purge_all_trash`, `popup-only.ts:420`); **no per-item purge UI**, though `purge_item` handler exists (`popup-only.ts:409`). CLI has single + bulk. |
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
| Capability | CLI | Ext | gap_class | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Attachments: add | full | partial | **real-gap** | Login/secure-note attachment editing gated behind `isInTab()` (`login.ts:370,388`; `secure-note.ts:123,140`) — unavailable in popup window; document renders unconditionally (`document.ts`). SW `upload_attachment` is full. |
|
||||||
|
| Attachments: view/download | full | full | at-parity | CLI `extract`; ext download + `download_attachment`. |
|
||||||
|
| Attachments: remove | full | partial | **real-gap** | SW helper `removeAttachmentsFromItem` (`vault.ts:492`) has **no router wire** (`remove_attachment` absent — confirmed). UI removes refs at form-save only, with the same `isInTab()` caveat. CLI `detach` is full. |
|
||||||
|
| TOTP: live code | none | full | real-gap (CLI-side) | CLI reveals raw base32 only; ext computes live codes. Extension leads. No spec mandates CLI OTP. |
|
||||||
|
| Generator: password / passphrase | full | full | at-parity | CLI `generate`; ext generator-panel + `generate_password`. |
|
||||||
|
| Settings: view / edit | full | full | at-parity | CLI `settings`; ext `get/set_vault_settings`. |
|
||||||
|
| Search | partial | partial | at-parity | CLI: title-substring. Ext: client-side over title/group/tags/icon_hint; SW `searchItems` (`vault.ts:316`) exists but **unwired** (no `search_items` message). Neither does field-value full-text. |
|
||||||
|
| Filter | full | partial | **real-gap** | CLI `list` filters type/group/tag/trashed. Ext: type filter is **vault-tab-only**; popup has none; **no tag filter anywhere**. SW `list_items` filters by `group` only. |
|
||||||
|
| Favorites | partial | **none** | **real-gap** | CLI add-only (`--favorite` on Login add, `*` in list; no toggle/filter). Ext: **zero UI** — `favorite` only round-trips. Ext strictly worse; needs a paired CLI+ext design. |
|
||||||
|
| Tags | full | partial | **real-gap** | CLI full create+filter all types. Ext: only Document form edits tags (`f-tags` in `document.ts`); no tag chips in lists; no tag filter. SW round-trips tags. |
|
||||||
|
| Groups/folders | full | partial | **real-gap** | CLI all types `--group`, `list --group`. Ext: only Login form has `f-group`+autocomplete; other forms set no group; vault-tab "group" filter is actually a type filter. SW `list_groups`/group-filter full. |
|
||||||
|
| Field history (view) | full | full | at-parity | CLI `history`; ext `get_field_history`. |
|
||||||
|
| Custom fields / sections | none | full | real-gap (CLI-side) | CLI has no custom-field/section commands (core supports them); ext `renderSectionsEditor` covers all 7 types. CLI backlog. |
|
||||||
|
| Autofill hostname matching | n/a | partial | **real-gap** | Ext-only feature; matcher (`vault.ts` `findByHostname`, `:344`) is exact `icon_hint` equality — no `www.` strip / eTLD+1. `www.x.com` ≠ `x.com`. (PM-surfaced.) |
|
||||||
|
|
||||||
|
### Org (enterprise) vault
|
||||||
|
|
||||||
|
| Capability | CLI | Ext | gap_class | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Org: read items | full | none | already-planned | CLI `org get/list` grant-filtered, all 7 types. Ext has zero org code. Planned Dev-D. Spec: `2026-06-06-relicario-enterprise-org-vault-design.md` § Extension — Org Context; ROADMAP "Extension org parity — read". |
|
||||||
|
| Org: write (add/edit/rm) | partial | none | already-planned | CLI write = Login/SecureNote/Identity only (`OrgAddKind`, `main.rs:560`; Card/Key/Document/Totp absent — v0.8.1 lift in flight). Ext none. Planned Plan B-2. Spec: `2026-06-20-relicario-v0.8.1-parity.md` § Out of scope. |
|
||||||
|
| Org: member/collection mgmt | full | none | cli-only-by-design | CLI full lifecycle (~19 subcommands). Ext none — org **admin** is intended CLI-only (high-trust, low-frequency). |
|
||||||
|
|
||||||
|
### Vault lifecycle / infra
|
||||||
|
|
||||||
|
| Capability | CLI | Ext | gap_class | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Vault init / setup | full | full | at-parity | CLI `init`; ext setup wizard + `create_vault`/`attach_vault`. |
|
||||||
|
| Git sync (pull/push) | full | partial | cli-only-by-design | CLI real `git pull --rebase`/`push`. Ext writes straight to host Contents API; no local graph (`ahead`/`behind` always 0). Functionally syncs; architecturally different by design (`extension/ARCHITECTURE.md`). |
|
||||||
|
| Device management | full | full | at-parity | CLI `device`; ext `renderDevices` + SW device CRUD. (GitHub/GitLab deploy-key API is the deferred edge.) |
|
||||||
|
| Backup / restore | full | full | at-parity | CLI `.relbak` + git-history bundling; ext `export/restore_backup`. `.git` bundling sub-aspect is cli-only-by-design (ext has no local repo). |
|
||||||
|
| Import (LastPass) | partial | partial | at-parity | Both LastPass-CSV only; other importers deferred both surfaces by policy. |
|
||||||
|
| Recovery QR | full | full | at-parity | CLI generate/unwrap; ext `generate/unwrap_recovery_qr`. Webcam scan deferred both. |
|
||||||
|
| Standalone generate (no vault) | full | none | cli-only-by-design (low-confidence) | CLI `generate`/`rate` work outside a vault; ext generator is embedded in login form + settings (needs unlocked vault). A browser extension lacks the "no vault" generator use-case a shell has. No spec; flag if user demand appears. |
|
||||||
|
|
||||||
|
### Intended CLI-only (no taxonomy row; recorded so they are not re-litigated as gaps)
|
||||||
|
|
||||||
|
| Capability | gap_class | Notes / spec |
|
||||||
|
|---|---|---|
|
||||||
|
| Recovery-QR file-write (`--out`) | cli-only-by-design | Negative API contract — no surface writes the payload to disk; absence *is* the security property. `2026-05-01-recovery-qr-design.md`. |
|
||||||
|
| Org delete-org push to remote | cli-only-by-design | Phase-1 delete-org is a local tombstone; pre-receive hook rejects protected-file deletion. Pushable delete-org is phase-2. `2026-06-06-...-design.md`. |
|
||||||
|
| `imgsecret embed` subcommand | cli-only-by-design | CLI disaster-recovery tool; the extension setup wizard's image flow covers the equivalent. |
|
||||||
|
| Password coloring (CLI TTY) | cli-only-by-design (inverted) | Ext shipped it (v0.5.1); CLI TTY parity deferred until demand. `2026-05-01-password-coloring-design.md` § Out of scope. |
|
||||||
|
| Shell completions | cli-only-by-design | No extension analogue. |
|
||||||
|
|
||||||
|
## Gap classification summary
|
||||||
|
|
||||||
|
- **Real extension gaps (extension work closes them):** per-item purge UI; attachment add/remove UI + `remove_attachment` wire + `isInTab()` gate; popup type filter + tag filter; tag editing on all forms; group editing on all forms; favorites UI; autofill registrable-domain matching; **org read** (specced); **org write** (specced, behind CLI type parity).
|
||||||
|
- **CLI-side gaps (extension already ahead — separate CLI backlog, NOT extension work):** Identity/Card/Key edit field coverage; Document view/edit; live TOTP code; custom fields/sections commands.
|
||||||
|
- **Intended CLI-only (not gaps):** git pull/push, `.git` backup bundling, org admin, `imgsecret embed`, recovery-QR file-write, shell completions, standalone generate.
|
||||||
|
- **Already planned / deferred:** org read (Dev-D), org write + org item-type breadth (Plan B-2), org attachments/multi-vault (behind org).
|
||||||
|
|
||||||
|
## Prioritized forward work (extension)
|
||||||
|
|
||||||
|
Only items where **extension work** closes the gap. CLI-side gaps and intended-CLI-only items are excluded.
|
||||||
|
|
||||||
|
| Pri | Item | Size | Why | Depends on |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **P0** | **Attachment remove + un-gate popup:** wire a `remove_attachment` router message to `removeAttachmentsFromItem` (`vault.ts:492`); drop the `isInTab()` gate so login/secure-note attachment **add & remove** work in the popup. Closes *both* the add half (row "Attachments: add") and remove half. | M | Backend exists and is unreachable — highest value-per-effort. The popup-mode gate is a UX cliff (can't manage login attachments without popping out). | — |
|
||||||
|
| **P0** | **Per-item purge UI:** surface the existing `purge_item` handler (`popup-only.ts:409`) as a per-row permanent-delete in the trash view (today only bulk `purge_all_trash`). | S | Pure UI wiring over an existing handler; CLI has single-item purge. | — |
|
||||||
|
| **P1** | **Group editing on all type forms:** add `f-group` + `wireGroupAutocomplete` to card/key/identity/totp/document (Login-only today). | M | SW (`list_groups`, group filter) already full; replicate one existing form pattern across 5 forms. | — |
|
||||||
|
| **P1** | **Tag editing on all type forms + tag chips in lists:** promote Document's `f-tags` to a shared affordance on all 7 forms. | M | SW round-trips tags fully; only Document edits them today. | — |
|
||||||
|
| **P1** | **Filter parity:** add a type filter to the popup (vault-tab has it) and a tag filter to popup + vault tab; optionally push type/tag params into `list_items`. | M | CLI filters type/group/tag; ext type filter is fullscreen-only, tag filter absent. | Tag editing (so tags exist to filter on) |
|
||||||
|
| **P2** | **Favorites (paired CLI + ext):** favorite toggle in detail/edit, favorites filter, star in list rows — and extend the CLI beyond add-only, to reach *true* parity per the parity philosophy. | M | Ext strictly worse than CLI (none vs partial); both surfaces weak. Write a short spec first. | — (spec TBD) |
|
||||||
|
| **P2** | **Autofill registrable-domain matching:** replace exact `icon_hint` equality (`vault.ts` `findByHostname:344`) with `www.`-strip + eTLD+1 matching. | S–M | `www.x.com` ≠ `x.com` today; the one personal-side quality gap. | — |
|
||||||
|
| **P2** | **Search wire-up (hardening):** expose `searchItems` (`vault.ts:316`) via a `search_items` message, or formally adopt client-side filtering and remove the dangling helper. | S | Functionally at-parity, but an unwired helper is dead-code drift. | — |
|
||||||
|
| **P3** | **Org read in extension (Dev-D):** org context switcher + SW org handlers (unwrap org master key into a `Zeroizing` session handle) + grant-filtered manifest browse/read in popup + vault tab. | XL | Largest single gap; entire org feature is CLI-only in the extension. Specced, deferred. | — |
|
||||||
|
| **P3** | **Org offline read-only indicator:** "org offline — writes disabled" banner when the git remote is unreachable in org context. | S | Spec-mandated UX. | Org read |
|
||||||
|
| **P3** | **Org SW acceptance tests:** org context replaces personal manifest cleanly; org master key never in localStorage/IndexedDB; offline mode triggers on network error. | M | Spec-mandated coverage following the feature. | Org read |
|
||||||
|
| **P3** | **Org write in extension (Plan B-2):** org add/edit/rm including Card/Key/Document/Totp. | XL | Closes org write + item breadth. Deferred past v0.8.1. | Org read **and** CLI reaching Card/Key/Document/Totp org-write parity (v0.8.1) |
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- Line citations are point-in-time against `b09e0ce` and drift with edits.
|
||||||
|
- This is a planning artifact, not a commitment; sizes are rough (S ≈ hours, M ≈ a day, L ≈ days, XL ≈ a multi-stream lift).
|
||||||
|
- Two analytical errors caught during cross-check and corrected here: (1) the struck `messages.ts`-doesn't-exist claim (file exists at `extension/src/shared/messages.ts`); (2) a few inventory line numbers were off by single digits and have been replaced with hand-verified ones.
|
||||||
Reference in New Issue
Block a user