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.
Review fix (Important secret-hygiene finding): export_state_bytes built a
PersistedDeviceState via state.signing_private.as_str().to_owned(), creating
non-zeroized heap copies of the device ed25519 private keys (signing + deploy)
that lingered as plaintext residue after the struct dropped.
- Cargo.toml: enable zeroize "derive" feature (zeroize_derive already in lock tree)
- device.rs: PersistedDeviceState now derives Zeroize + ZeroizeOnDrop, wiping all
String fields on every drop path (success, serde_json::to_vec ? error path, panic)
- device.rs: import_state_bytes uses std::mem::take per field (ZeroizeOnDrop adds a
Drop impl, so fields can no longer be moved out — E0509); mem::take transfers the
heap buffers without copying, leaving empty Strings the drop-zeroize no-ops
- doc-comments updated to describe the ZeroizeOnDrop / mem::take hygiene
Hygiene-only; no behavior change. Gates: cargo test -p relicario-wasm 15/15,
clippy -D warnings clean, build --target wasm32-unknown-unknown clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
- 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
Universal isomorphic-git receive-pack push path shipped & verified: sshsig.ts +
mem-fs.ts + git-push.ts + commitSigned on GitHost/Gitea/GitHub. Full SW suite
163/163, build:all bundles isomorphic-git into the SW, /security-review clean.
Tasks 3-5 remain HELD on Dev-A + Dev-B merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
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