The full-suite meta-test (src/shared/__tests__/error-copy.test.ts) requires a
user-facing ERROR_COPY entry for every literal 'ok: false, error: <code>' the SW
returns. The org handlers added org_not_configured (literal) with no copy → RED.
Add org_not_configured plus copy for the org codes that reach the popup via
thrown err.message (not_an_org_member, device_key_unavailable, not_in_org_context,
org_switch_failed). item_not_found already had an entry. (Dev-B added some of
these independently; B reconciles the wording when it syncs main post-merge.)
Full extension vitest: 70 files / 478 tests green (incl. error-copy 3/3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
A's org foundation returns org_not_configured (ok:false) plus thrown not_an_org_member /
device_key_unavailable, none of which had user-facing copy — they would render as the raw
code. Add friendly copy (org_not_configured ties into the add-org flow) and a focused test;
fixes the ERROR_COPY-coverage test that the foundation merge left red.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
A-vs-E keep-both integration in 3 security-sensitive SW files:
- shared/messages.ts: kept E's attach_vault keyfile second-factor fields
(secondFactor/referenceImageBytes?/keyfileBytes?) AND my 4 org message types.
- vault.ts: registerDeviceAndPersistConfig auto-merged to take BOTH E's
factorStorage (image|keyfile) AND my handle (Task 4.5 device-key persist);
both create/attach call sites pass (factorStorage, deviceName, h) so each
flow does E secret-gen AND my register+persist in one coherent path.
- router/popup-only.ts: took E's 'unlock' -> handleUnlock refactor and moved my
device-key restore + migration into handleUnlock's shared tail (after
setCurrent, before manifest fetch) so it runs for both image + keyfile unlock.
- keyfile-unlock.test.ts: makeWasm mock gains persist_device_key + restore_device_key
stubs (the merged create/attach/unlock flow now calls them) — no assertion weakened.
Gates: SW vitest 175/175 (18 files); cargo core+wasm 243/0; wasm32 build clean;
clippy -D warnings clean; build:all clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Security review I1: openOrg's org_unwrap_key inserts the org master key into the
WASM session at step 7, but on the non-cached manifest-error path the handle was
never returned / registered via session.setOrg — so session.clearAll() (lock +
inactivity timeout) could not see it, and the org key would outlive a failed open
(GC-backstopped, non-deterministic). Triggers on a corrupt manifest.enc or a
first-switch network error with no cache. Free the handle before rethrowing.
Adds a test asserting the error path frees the handle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Wires 4 SW messages to the org-vault.ts read functions (Task 5b):
- org_switch: opens org vault via openOrg(), caches OrgHandleState, sets
session context; switching to 'personal' resets context without openOrg.
Offline-aware: if openOrg throws a network error and a prior cached state
exists, reuses it read-only (offline:true) via exported isNetworkError.
- org_list_items: returns cached, grant-filtered OrgManifestEntry[] (not
personal ManifestEntry[]) via listOrgItems(requireCurrentOrgState()).
- org_get_item: decrypts a single org item via getOrgItem.
- org_list_collections: returns granted Collection[] via listOrgCollections.
Three-place rule satisfied for all 4 messages:
1. messages.ts PopupMessage union (4 variants)
2. messages.ts POPUP_ONLY_TYPES (4 entries)
3. popup-only.ts switch (4 case arms)
Cache lifecycle / use-after-free guard:
- clearOrgCache() added to org-handlers.ts; exports.
- Called in popup-only.ts 'lock' handler (before session.clearCurrent).
- Called in index.ts inactivity-timer onExpired (before clearCurrent).
Plan doc fixes:
- Line ~410: org_list_items return type corrected to OrgManifestEntry[].
- Line ~47: org_unwrap_key signature updated (no device_private_key_base64
param — reads DEVICE_STATE internally).
9 new tests in org-handlers.test.ts (TDD: all watched fail before passing).
Full SW suite: 167 tests (17 files) green. build:all type-checks clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Add OrgManifest::filter_by_collections(&[String]) as the single source of
grant-filter logic. Refactor filter_for_member to delegate to it (no
duplicated loop). Add org_manifest_decrypt_filtered WASM binding that
decrypts + filters in core so ungranted entries never cross to JS (phase-1
SOFT/UX filter; per-collection crypto isolation is phase-2). Declare the
binding in extension/src/wasm.d.ts. Vec<String> compiled cleanly for
wasm32-unknown-unknown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
- Add `persistDeviceKey(w, handle)` to vault.ts: calls
`w.persist_device_key(handle)` and stores the ciphertext as base64
in chrome.storage.local `device_key_enc`.
- Wire into `registerDeviceAndPersistConfig` (create + attach flows)
so the key is persisted immediately after every `register_device`.
- Wire into `register_this_device` handler via `session.requireCurrent()`.
- Restore at unlock: if `device_key_enc` present, call
`w.restore_device_key(handle, blob)`; failure is caught + logged but
must not block unlock.
- Migration for pre-4.5b installs (absent `device_key_enc`): re-register,
persist new key, addDevice to remote (best-effort, must not block).
New device identity — org members must be re-granted to new pubkey.
- 4 new tests in device-key-persist.test.ts; existing 146 tests updated
and kept green; router.test.ts register_this_device stub extended.
- Add KEYFILE_PARAMS_JSON const (same KDF params as default + second_factor:"keyfile" hint)
- Generalize registerDeviceAndPersistConfig: takes factorStorage union instead of
referenceImageBytes Uint8Array; spreads { imageBase64 } or { keyfileBase64 } into
chrome.storage.local.set so exactly one factor key is present
- Branch handleCreateVault on msg.secondFactor === 'keyfile': skip carrier-image
guards and embed_image_secret; call unlock_with_secret + keyfile_encode; share
the file-write / device-registration / session tail with the image path
- Update handleAttachVault call to registerDeviceAndPersistConfig to pass
{ imageBase64: uint8ArrayToBase64(referenceImageBytes) }
- Relax carrierImageBytes to optional in messages.ts create_vault request type
- Skip Task-4 describe block in keyfile-unlock.test.ts (un-skip in Task 4)
TDD: Task-2 test RED → GREEN; Task-4 describe.skipped; full suite 439 passed/1 skipped; build clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
Additive conflict in extension/src/wasm.d.ts resolved by keeping BOTH the
org_* declarations (org_unwrap_key/org_manifest_decrypt/org_manifest_encrypt)
and the keyfile_* declarations (unlock_with_secret/keyfile_encode/keyfile_decode).
Rust lib.rs (core + wasm) auto-merged cleanly with both feature sets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
PushResult has a singular `error: string | null` and a `refs` map — there
is no top-level `errors: string[]`. Replace the fabricated type and the
`errors`-based rejection guard with a correct PushResultRuntime type and a
belt-and-suspenders refsOk check. The real rejection path is a thrown
GitPushError (isomorphic-git throws, never returns, when a hook declines);
replace the single fiction-testing test with (a) a throw-path test that mocks
push to reject with a GitPushError and (b) a defensive non-throwing test that
mocks push to resolve { ok: false, error: 'unpack failed' }.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
Add CommitFile/CommitSigner types + commitSigned to GitHost interface.
Implement git-push.ts (clone→add→commit({onSign=SSHSIG})→push over http/web)
and wire GiteaHost.commitSigned / GitHubHost.commitSigned. Tests: 7/7 incl.
golden-vector tie against the sshsig.test.ts byte-exact vector.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
Introduces OrgConfig/OrgConfigSummary types, chrome.storage.local backing
via org-config.ts, and the first org SW message (org_list_configs) wired in
all three required places: PopupMessage union, POPUP_ONLY_TYPES set, and a
case arm in popup-only.ts. Handler projects to OrgConfigSummary — apiToken,
hostUrl, repoPath, memberId never leave the SW. 3 new TDD tests (RED→GREEN).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
clearAll is the security-zero path: each .free() in its own try/catch so a
single throwing handle can't leave the others un-zeroed; state is fully reset
regardless. setOrg frees any prior handle for the same org before overwriting
so a re-unlock can't orphan an un-zeroed WASM key. Tests updated to the
best-effort contract (session.test.ts) plus independent-mock, throwing-free,
and re-unlock cases (org-session.test.ts).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
PM contract refinement: org_list_items returns a DEDICATED OrgManifestEntry[]
(collection REQUIRED), not the personal ManifestEntry-with-optional-collection.
Dev-A is mirroring OrgManifest/OrgManifestEntry into shared/types.ts — the list
imports that type at integration (no hand-rolling). Behaviors unaffected; scaffold
stays RED (6 fail / 3 pass). Fixture-only change, no src.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
Adds three WASM bindings for the pluggable second-factor work:
- unlock_with_secret: mirrors unlock() but accepts a raw 32-byte secret
instead of a carrier image, skipping imgsecret::extract
- keyfile_encode: armors a 32-byte secret into relicario-keyfile-v1 format
- keyfile_decode: decodes keyfile armor back to the raw 32-byte secret
Adds native equivalence test proving that unlock_with_secret and unlock
derive an identical Argon2id master key when given the same secret,
passphrase, salt, and KDF params. Comparison is direct (master-key bytes),
not cross-decrypt, so the test is both native-safe and unambiguous.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
Expose org_unwrap_key(keys_blob, device_private_openssh) to let the
extension SW unwrap a member's ECIES-wrapped org master key into an
ordinary Zeroizing session handle. The returned handle is key-agnostic
and works with the existing item_decrypt/manifest_decrypt AEAD.
Key-form finding: device private keys are OpenSSH PEM blobs produced by
relicario_core::device::generate_keypair(), not base64 raw bytes. The
plan's "device_private_key_base64" parameter name was misleading; all
code and the .d.ts declaration use "device_private_openssh" to match
the spec and reality.
Adds ssh-key dep to relicario-wasm for PEM parsing in decode_device_seed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Extension-only patch: fixes vault create/attach in the browser extension, where
chrome.runtime.sendMessage silently dropped ArrayBuffer payloads (carrier JPEG →
0 bytes → opaque "no SOF marker found in JPEG"). See CHANGELOG for the full set.
- Version bumps: extension 0.7.0→0.8.2 (it had no changes in 0.8.0/0.8.1);
relicario-core/-cli/-wasm 0.8.1→0.8.2 for unified-tag consistency. The Rust
core/CLI are functionally unchanged from v0.8.1. relicario-server stays 0.1.1
(independent version line; no functional change).
- CHANGELOG.md: v0.8.2 section. STATUS.md / ROADMAP.md: v0.8.2 landed, v0.9.0
flagged as ready to kick off.
Verify: cargo build/test/clippy -D warnings all green (35 test binaries);
extension 435/435 vitest, build:all clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
- Closed vault drawer now negates its 440px via margin-right so it reserves no
layout space — the list pane and its empty state center in the full
post-sidebar width instead of being shifted left by the off-screen drawer.
- .vault-lock-screen gets flex:1 so the locked vault tab grows to full viewport
width instead of collapsing to content width and pinning to the left.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
chrome.runtime.sendMessage serializes with a JSON-like algorithm that drops
ArrayBuffer/TypedArray payloads — they arrive as empty objects, so a carrier
JPEG reached the service worker as 0 bytes and failed with the opaque WASM
error "no SOF marker found in JPEG". This broke extension vault creation.
- shared/message-binary.ts: encodeBinary/decodeBinary deep-walk a message tree
and base64-envelope every binary buffer so it survives the channel (chunked
base64 to avoid stack overflow on multi-MB buffers); plain number[] untouched.
- Wire encode/decode at all four message boundaries: popup, service-worker
onMessage listener (decode request, encode response), setup swSend, vault
postToServiceWorker.
- handleCreateVault: magic-byte guard classifies empty / non-JPEG carriers into
actionable errors (carrier_image_empty / carrier_image_not_jpeg) instead of
the opaque WASM error; error-copy.ts adds user-facing text; setup errors now
route through lookupErrorCopy.
- router SETUP_ALLOWED: add create_vault / attach_vault / generate_recovery_qr —
the Phase-3 setup wizard sends these from the setup page and they were being
rejected as unauthorized_sender.
- Tests: message-binary round-trip suite (7) + carrier-guard (2) + setup
allowlist (3). 435/435 vitest, build:all clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
Completes the extension restructure begun in v0.6.0. Phases 3 (setup
wizard SW migration + step registry), 4 (vault.ts split + vault_locked
lift), and 6 (get_vault_status + sidebar status indicator) all merged to
main (9df2fee, 3b8368d, 397cc78) via three parallel worktree streams.
This commit is the release-prep wrap-up:
- Version bump to v0.7.0 across the three relicario crates + Cargo.lock,
extension/package.json, and both extension manifests (the manifests had
lagged at 0.5.0 — corrected here).
- CHANGELOG.md v0.7.0 entry.
- STATUS.md: extension restructure moved to shipped; Phases 3/4/6 landing
section added.
- ROADMAP.md: v0.7.0 row added; Up-next now command palette.
- extension/ARCHITECTURE.md: all three phases integrated (new vault-*
modules, setup-steps.ts, get_vault_status protocol + status indicator,
vault_locked lift, git-host sync cache).
- Plan completion checkboxes ticked.
Task 7.1 verification: done-criteria sweep all green; 423/423 vitest;
build:all clean (only the pre-existing 4MB WASM size warning).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The explicit lock message handler nulled state.manifest but left
state.gitHost (now carrying the cached lastSyncAt) intact, so a lock then
re-unlock within one service-worker lifetime surfaced a stale sync time.
Null gitHost here too — symmetric with the session-expiry path (index.ts)
and completing Plan C Phase 5's don't-leak-git-host-across-a-lock intent;
unlock rebuilds it on demand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>