docs(ext/sw): tighten slice-3 comments per code review

Non-functional tightening flagged in the slice-3 code review:

- session.ts: document future multi-vault refactor (β+) so the module-
  scope singleton is explicitly "deliberately simple," not an oversight.
- vault.ts: move findByHostname doc comment above the function; note
  α's intentionally-coarse hostname match (no www-stripping, no
  public-suffix matching) and that tighter matching is a β/γ concern.
- index.ts: expand the passphrase scope-clearing comment to make
  the theatre explicit rather than leaving it looking like real defense.
- index.ts: TODO(slice-4) marker on delete_item's non-atomic two-write
  path — consider manifest-first ordering or retry/rollback at router-
  split time.
- index.ts: cross-reference comment on itemToManifestEntry pointing at
  the Rust-side ManifestEntry::from_item derivation it must mirror.

No behavior change; build still compiles with 2 bundle-size warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-20 20:07:27 -04:00
parent c0fba2a8dc
commit 2fd6daad8e
3 changed files with 27 additions and 4 deletions

View File

@@ -122,8 +122,12 @@ async function handleMessage(req: Request): Promise<Response> {
const handle = w.unlock(req.passphrase, imageBytes, meta.salt, meta.paramsJson); const handle = w.unlock(req.passphrase, imageBytes, meta.salt, meta.paramsJson);
session.setCurrent(handle); session.setCurrent(handle);
// Clear passphrase from scope best-effort. // Best-effort scope clearing. JS strings are immutable so this only
// (JS strings are immutable; the message object goes out of scope after return.) // nulls the one reference on `req`; wasm-bindgen already copied the
// string into WASM linear memory before `unlock` returned. Left in
// as a marker — future work may consider a direct-stream KDF that
// never materializes the string, or a `Zeroizing` wrapper on the
// WASM-side incoming buffer.
(req as { passphrase: string }).passphrase = ''; (req as { passphrase: string }).passphrase = '';
manifest = await vault.fetchAndDecryptManifest(git, handle); manifest = await vault.fetchAndDecryptManifest(git, handle);
@@ -178,6 +182,10 @@ async function handleMessage(req: Request): Promise<Response> {
const entry = manifest.items[req.id]; const entry = manifest.items[req.id];
if (!entry) return { ok: false, error: 'item_not_found' }; if (!entry) return { ok: false, error: 'item_not_found' };
// Soft-delete: fetch the item, set trashed_at, write it back. // Soft-delete: fetch the item, set trashed_at, write it back.
// TODO(slice-4): not atomic across the two git writes. If the manifest
// write fails after the item write, next sync restores the live manifest
// and the trashed item re-appears. Consider manifest-first write order
// or a retry/rollback pass when router-splitting this handler.
const item = await vault.fetchAndDecryptItem(gitHost, handle, req.id); const item = await vault.fetchAndDecryptItem(gitHost, handle, req.id);
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const updated: Item = { ...item, trashed_at: now, modified: now }; const updated: Item = { ...item, trashed_at: now, modified: now };
@@ -253,6 +261,12 @@ async function handleMessage(req: Request): Promise<Response> {
} }
} }
/// TS mirror of the Rust core's `ManifestEntry::from_item`
/// (see crates/relicario-core/src/manifest.rs). These two derivations must
/// stay in sync — if the Rust side learns to handle e.g. trailing-whitespace
/// URL parsing differently, this function drifts and the manifest diverges
/// between CLI-written and extension-written items. A future WASM helper
/// `item_to_manifest_entry(handle, item_json)` would eliminate the duplication.
function itemToManifestEntry(item: Item) { function itemToManifestEntry(item: Item) {
return { return {
id: item.id, id: item.id,

View File

@@ -3,6 +3,10 @@
/// α assumes one vault per extension install. The master key lives only /// α assumes one vault per extension install. The master key lives only
/// inside WASM linear memory (wrapped in Zeroizing<[u8;32]>); this module /// inside WASM linear memory (wrapped in Zeroizing<[u8;32]>); this module
/// just holds the opaque handle that names it. /// just holds the opaque handle that names it.
///
/// Future multi-vault (β+) would replace `current` with
/// `Map<vaultId, SessionHandle>` and thread `vaultId` through every
/// handler. Deliberate α simplicity — not an oversight.
import type { SessionHandle } from '../../wasm/relicario_wasm'; import type { SessionHandle } from '../../wasm/relicario_wasm';

View File

@@ -124,6 +124,13 @@ export function searchItems(
}); });
} }
/// Match manifest entries against a page hostname.
///
/// icon_hint is derived by the Rust core (crates/relicario-core/src/manifest.rs)
/// from LoginCore.url's hostname, so equality on icon_hint is the cheapest match.
/// α is intentionally coarse: no www.-stripping, no public-suffix matching
/// (`www.github.com` saved items will not match `github.com`, and vice versa).
/// Tighter matching is a 1C-β/γ concern.
export function findByHostname( export function findByHostname(
manifest: Manifest, manifest: Manifest,
hostname: string, hostname: string,
@@ -132,6 +139,4 @@ export function findByHostname(
return (Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>) return (Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>)
.filter(([, e]) => e.trashed_at === undefined) .filter(([, e]) => e.trashed_at === undefined)
.filter(([, e]) => (e.icon_hint ?? '').toLowerCase() === h); .filter(([, e]) => (e.icon_hint ?? '').toLowerCase() === h);
// icon_hint is derived by Rust core from LoginCore.url's hostname,
// so hostname equality on icon_hint is the cheapest match.
} }