Commit Graph

360 Commits

Author SHA1 Message Date
adlee-was-taken
df2954dd26 fix(ext/sw): org_add_item mints a real item id (C1 data-loss) + signer hardening
C1 (CRITICAL, caught by the PM's independent reviewer; masked by a test that
hard-coded the id): the popup sends id:'' for NEW items — the personal add_item
handler mints one via new_item_id(), but handleOrgAddItem used item.id verbatim.
Every new org item therefore wrote items/<collection>/.enc, so the 2nd add to a
collection OVERWROTE the first (blob data loss) + accumulated duplicate empty-id
manifest entries. Fix: mint the id with w.new_item_id() and use it for the blob
path, the manifest entry, and the encrypted item.

Also:
- m4: sanitize the device name in buildSigner (strip CR/LF/<>) + derive a safe
  email local-part, so a crafted device name can't malform the signed commit's
  author line.
- m1: add update/delete ungranted-collection refusal tests (parity with add).
- Regression test: org_add_item with id:'' now asserts a real 16-hex id lands in
  the path + manifest entry (the test that masked C1 is fixed to mint the id).
- m3 is eliminated by the C1 fix; m2 (TOCTOU dual-read) + m5 (zero-grant +new /
  push-rejection copy) tracked as non-blocking follow-ups.

Full extension suite 584/584; build:all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
2026-06-27 18:23:39 -04:00
adlee-was-taken
1ce37f714e fix(ext): org writes refresh + re-sync the cached org manifest (visible after add/update/delete)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
2026-06-27 14:34:27 -04:00
adlee-was-taken
61fbafaa30 feat(ext): org write UI — collection picker, edit/delete, offline-gated across all 7 item types
- Add write-routing.ts: writesEnabled(), messageForAdd/Update/Delete(),
  collectionPickerHtml(), selectedCollectionSlug() — shared helpers that
  route to personal or org SW messages by context, gate on orgOffline.
- Un-hide edit/trash buttons in all 7 type-module detail views; render
  them disabled when writesEnabled() is false (org offline).
- Add if (!writesEnabled()) return guard to edit/trash click handlers and
  keyboard 'e'/'d' shortcuts across all 7 modules.
- Route delete through messageForDelete() in all 7 modules (personal →
  delete_item, org → org_delete_item).
- Insert collectionPickerHtml() into add forms across all 7 modules;
  route save through messageForAdd/Update() using selectedCollectionSlug().
- Un-gate '+ new' button and keyhint in item-list.ts: shows when
  writesEnabled() (personal always; org when online); hides when offline.
- Fix pre-existing error-copy gap: add org_offline and
  collection_not_granted entries (Task 3 SW handlers emitted these codes
  without corresponding user-facing copy).
- Tests: write-routing.test.ts (21 tests, truth-table + route assertions);
  org-write-ui.test.ts (13 tests, collection picker / personal unchanged /
  offline disable / org_delete_item routing). Updated item-list.test.ts
  and detail-read-only.test.ts to reflect new online-org behavior.
2026-06-27 14:20:39 -04:00
adlee-was-taken
50a05affcb feat(ext/sw): org_add/update/delete_item — signed commits via commitSigned + manifest landmine guard
Implements Task 3 of v0.9.0 Plan C: three org-write SW handlers in org-handlers.ts.
Each handler decrypts the FULL manifest via org_manifest_decrypt (unfiltered) before
mutating it — never state.manifest (filtered) — preventing silent wipe of ungranted
collection entries on re-encrypt. Grant checked before any network call for add;
checked post-lookup for update/delete. Delete is manifest-only (soft-delete via
trashed_at); item blob stays on disk. All three return org_offline when offline.
Commit signer built from get_device_info + rawSign wrapping sign_for_git output.

Wired: PopupMessage union + POPUP_ONLY_TYPES (messages.ts) + dispatch (popup-only.ts).
Tests: 7/7 org-write.test.ts (RED→GREEN); full SW suite 210/210; tsc clean.
2026-06-27 13:54:53 -04:00
adlee-was-taken
e0dd5590bc Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-c-org-write 2026-06-27 13:42:49 -04:00
adlee-was-taken
39fe7bf6e2 Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-b-org-read-ui
# Conflicts:
#	extension/src/shared/error-copy.ts
2026-06-27 13:35:18 -04:00
adlee-was-taken
7c6e6fe32a feat(ext): org error-copy entries for ERROR_COPY coverage gate
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
2026-06-27 13:15:21 -04:00
adlee-was-taken
cc4d4e8f36 fix(ext): enforce org read-only in detail/drawer + context-aware vault drawer + escape switcher options 2026-06-27 12:52:36 -04:00
adlee-was-taken
59b0fcd985 feat(ext): add-org form + switcher trigger (org_add_config UI)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
2026-06-27 12:28:24 -04:00
adlee-was-taken
11e3aa41d1 feat(ext/sw): org_add_config message + handler (device-local org config write)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
2026-06-27 12:23:57 -04:00
adlee-was-taken
e370b06f85 feat(ext): org collection facet + collection filter in the vault sidebar
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
2026-06-27 12:17:36 -04:00
adlee-was-taken
dd0b415cdf refactor(ext): load org entries in the shared switcher (works in popup + vault tab)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
2026-06-27 12:01:06 -04:00
adlee-was-taken
c327c21fdc feat(ext): list/detail load + render org items in org context (normalized, read-only) 2026-06-27 11:47:50 -04:00
adlee-was-taken
183a3f0a81 fix(ext): ERROR_COPY entries for org context errors (read-UI surfacing)
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
2026-06-27 11:37:42 -04:00
adlee-was-taken
37fb76ab97 feat(ext): org context switcher (popup header + vault sidebar) + offline read-only banner 2026-06-27 11:31:20 -04:00
adlee-was-taken
eae07e778b merge: origin/main (Dev-E keyfile-ext + D-minors) into org-foundation
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
2026-06-27 11:22:17 -04:00
adlee-was-taken
aceeb30af9 feat(ext): PopupState org fields + context-aware message helper org branches 2026-06-27 11:21:31 -04:00
adlee-was-taken
7913e3ad84 fix(ext/sw): free org handle on openOrg error path (no org-key leak past lock)
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
2026-06-27 11:05:58 -04:00
adlee-was-taken
8b97ebc01a Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-c-org-write 2026-06-27 11:04:23 -04:00
adlee-was-taken
72988eb481 Merge remote-tracking branch 'origin/feature/v0.9.0-dev-a-org-foundation' into feature/v0.9.0-dev-b-org-read-ui 2026-06-26 23:06:29 -04:00
adlee-was-taken
7a6cfb5659 test(ext/sw): cover handleUnlock keyfile error paths (forward-compat guard, invalid_key_file, key-file-not-set) 2026-06-26 22:51:10 -04:00
adlee-was-taken
f9b8f3821d feat(ext/sw): org_switch + org read messages (grant-filtered, offline-aware, cache cleared on lock)
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
2026-06-26 22:45:09 -04:00
adlee-was-taken
2eb49ac217 feat(ext/sw): org-vault read core (openOrg, grant-filtered list, collection-scoped get, offline)
- Add extension/src/service-worker/org-vault.ts: openOrg (members.json fingerprint
  match → org_unwrap_key → org_manifest_decrypt_filtered), listOrgItems, getOrgItem
  (collection-scoped path: items/<collection>/<id>.enc), listOrgCollections (grants filter).
- Add OrgMember, OrgMembers, CollectionDef, OrgCollections, Collection types to shared/types.ts
  (mirrors relicario-core org.rs shapes).
- Add 8-test suite covering: fingerprint match, not_an_org_member, listOrgItems passthrough,
  collection-scoped item path, listOrgCollections grant filter, storage.set never called,
  offline fallback (TypeError → cached manifest), offline with no cache → rethrow.
- Fix plan doc line ~326: wasm.manifest_decrypt → wasm.org_manifest_decrypt_filtered.

Gate results: 158/158 SW tests pass; npm run build:all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 22:30:39 -04:00
adlee-was-taken
288956c089 feat(core,wasm): filter_by_collections single-source + org_manifest_decrypt_filtered
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
2026-06-26 22:19:55 -04:00
adlee-was-taken
201bf05adc feat(ext/setup+sw): attach via key file when the vault uses one (probe second_factor, .relkey picker, handleAttachVault keyfile branch)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 22:19:41 -04:00
adlee-was-taken
0d56b3333b feat(ext/sw): persist device key at register, restore at unlock (+ migration)
- 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.
2026-06-26 22:13:50 -04:00
adlee-was-taken
2f7a034a1b feat(ext/setup): download the generated .relkey at finish + skip carrier gate in keyfile mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 22:07:14 -04:00
adlee-was-taken
d3f46389e4 feat(ext/sw): unlock resolves second factor from params hint (keyfile branch)
Extract handleUnlock from popup-only.ts handle() switch, add keyfile branch
(keyfile_decode + unlock_with_secret), forward-compat guard for unknown
second_factor values, loadKeyfileBase64 helper, loadSetupState isConfigured
fix for keyfile vaults. Register error copy for three new error codes.
Un-skip Task 4 TDD test — 440/440 green, build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 21:59:17 -04:00
adlee-was-taken
be619f8ed0 feat(wasm): persist/restore device key under master key; org_unwrap_key reads DEVICE_STATE
- device.rs: add PersistedDeviceState (serde mirror), export_state_bytes
  (plaintext JSON in Zeroizing<Vec<u8>>, NOT a wasm_bindgen export),
  import_state_bytes (repopulates DEVICE_STATE with Zeroizing<String> keys),
  signing_seed (extracts ed25519 seed via core::device::extract_ed25519_seed)
- lib.rs: add persist_device_key (#[wasm_bindgen], encrypts state bytes under
  master key via relicario_core::crypto::encrypt; ciphertext only reaches JS),
  restore_device_key (#[wasm_bindgen], decrypts inside WASM then calls
  import_state_bytes; plaintext never leaves WASM)
- lib.rs: refactor org_unwrap_key to drop device_private_openssh param; reads
  seed from DEVICE_STATE via device::signing_seed()
- lib.rs: update org_unwrap_key_yields_a_session_that_decrypts_org_blobs to
  new signature (register_device first, use signing_pub for wrap_org_key)
- lib.rs: add device_persist_tests module (4 tests: round-trip, org-unwrap
  post-restore, ciphertext-not-plaintext, no-device-error)
- lib.rs: add DEVICE_TEST_LOCK static to serialize DEVICE_STATE-touching tests
- lib.rs: add #[derive(Debug)] to SessionHandle
- wasm.d.ts: update org_unwrap_key sig; declare persist_device_key +
  restore_device_key

All gates: cargo test -p relicario-wasm (15/15), cargo test -p relicario-core
(no regression), cargo build --target wasm32-unknown-unknown (clean), clippy
-D warnings (clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 21:53:24 -04:00
adlee-was-taken
5cc16ddffb feat(ext/sw): create_vault key-file mode (generate secret, unlock_with_secret, store keyfileBase64)
- 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
2026-06-26 21:49:09 -04:00
adlee-was-taken
ce141480b9 merge: origin/main (Dev-D keyfile, d649203) into org-foundation
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
2026-06-26 21:35:56 -04:00
adlee-was-taken
6970893713 Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-e-keyfile-ext 2026-06-26 21:35:39 -04:00
adlee-was-taken
956ab6886a Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-c-org-write 2026-06-26 21:35:17 -04:00
adlee-was-taken
478dc88296 Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-b-org-read-ui 2026-06-26 21:34:03 -04:00
adlee-was-taken
d942650f9f fix(ext/sw): model isomorphic-git PushResult correctly; test the real push-rejection throw
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
2026-06-26 00:19:34 -04:00
adlee-was-taken
56dec78f44 feat(ext/sw): universal commitSigned — isomorphic-git receive-pack push + SSHSIG
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
2026-06-26 00:08:09 -04:00
adlee-was-taken
0bc1f53b48 feat(ext/sw): mem-fs — dep-free in-memory fs for isomorphic-git org pushes 2026-06-25 23:51:00 -04:00
adlee-was-taken
f5336e352f feat(ext/sw): org config storage + org_list_configs message
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
2026-06-25 23:40:44 -04:00
adlee-was-taken
273fe5cf30 fix(ext/sw): sshsig hashes the exact payload view; hoist TextEncoder 2026-06-25 23:40:16 -04:00
adlee-was-taken
b3db292474 feat(ext/sw): sshsig — SSHSIG framing over sign_for_git for git-valid commit signatures 2026-06-25 23:33:42 -04:00
adlee-was-taken
2ecd98208d feat(wasm,ext): org_manifest_decrypt/encrypt over WASM + faithful OrgManifest TS mirror
- Add `org_manifest_decrypt` + `org_manifest_encrypt` to relicario-wasm/src/lib.rs,
  placed after `org_unwrap_key`; mirror the existing manifest_decrypt/manifest_encrypt
  pattern exactly (need_key guard, session::with, js_value_for). Core import path:
  relicario_core::decrypt_org_manifest / relicario_core::encrypt_org_manifest (crate root).
- Add round-trip test `org_manifest_round_trip_via_handle` in org_tests: encrypt via
  WASM wrapper, decrypt via core directly (avoids serde_wasm_bindgen off-wasm).
  Asserts collection/title/tags/schema_version survive the round-trip; asserts nonce
  uniqueness. TDD: RED (compile error) → GREEN confirmed.
- Declare org_manifest_decrypt/org_manifest_encrypt in extension/src/wasm.d.ts.
- Add faithful/lean OrgManifestEntry + OrgManifest TS interfaces to
  extension/src/shared/types.ts: {id, type, title, tags[], modified, trashed_at?,
  collection} — no personal-only fields; collection required. Mirrors org.rs:184-202.
- Plan-doc: rewrite Task 2 body (re-scope rationale + checked steps), fix File
  Structure + Architecture lines, amend Hand-off contract (org_list_items ->
  OrgManifestEntry[]; org items remain personal Item).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 23:33:29 -04:00
adlee-was-taken
f85dc6729e fix(ext/sw): best-effort clearAll + setOrg frees displaced handle
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
2026-06-25 21:40:37 -04:00
adlee-was-taken
c79ff1caf5 scaffold(ext): create_vault secondFactor request field + relkeyBytes response (held types) 2026-06-25 21:35:34 -04:00
adlee-was-taken
6bdb7cf262 feat(ext/sw): multi-context session (personal + orgs), clearAll zeroes all
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 21:29:08 -04:00
adlee-was-taken
20357d698e test(ext): refine org_list_items fixture to OrgManifestEntry shape
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
2026-06-25 21:28:57 -04:00
adlee-was-taken
6cb12990ec feat(wasm): unlock_with_secret + keyfile encode/decode bindings + equivalence proof
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
2026-06-25 21:24:10 -04:00
adlee-was-taken
986c5e1cb0 test(ext): failing-test scaffold for org read UI (Plan B Tasks 1-5)
Failing tests written against Dev-A's published org SW message contract
(org_list_configs / org_switch / org_list_items / org_get_item /
org_list_collections), plus compile-clean stubs. vitest: 6 fail / 3 pass —
the 3 passes are A-independent (currentContext default + org reflect, personal
"+ new" kept); the 6 fails are exactly the org behaviors HELD until Dev-A
merges to main.

Integration (flip-on at Dev-A merge):
- org-context messageForList/messageForGet org branches (org_list_items/org_get_item)
- org-switcher org_list_configs fetch + org_switch dispatch + list reload + offline banner
- item-list org initial-load-on-render (org_list_items) + hide "+ new" in org context
- collection facet populate from org_list_collections + click-to-filter

Scaffold contents:
- shared/org-context.ts (+__tests__): currentContext complete; messageFor* personal-only (HOLD)
- shared/popup-state.ts: optional orgContext / orgOffline fields (non-breaking; A-typed
  orgConfigs/orgCollections join at integration)
- popup/components/org-switcher.ts (+__tests__): select shell only (HOLD switch + Task 5 banner)
- popup/components/__tests__/item-list.test.ts: org load + read-only (+ new hidden) assertions
- vault/vault-sidebar.ts renderCollectionFacet (+__tests__): empty container (HOLD)

Read-only this plan: write affordances stay hidden (Dev-C adds them). No org-message-type
references in src — branch stays compile-clean; build:all type-check is the integration gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
2026-06-25 21:09:56 -04:00
adlee-was-taken
a25f8e8a30 feat(ext/setup): second-factor container choice (image | key file) 2026-06-25 21:06:58 -04:00
adlee-was-taken
d55861ffd8 feat(wasm): org_unwrap_key — ECIES unwrap into a session handle
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
2026-06-25 21:02:54 -04:00
adlee-was-taken
5a5cf04b8f release: v0.8.2 — extension vault-creation fix (binary transport)
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
2026-06-25 20:26:36 -04:00