From 332ab647c9b9be1655439837feea2872ef10d8e3 Mon Sep 17 00:00:00 2001
From: Avi
Date: Sat, 26 Sep 2026 20:34:08 -0500
Subject: [PATCH 01/17] fix(nip46): gate remaining trace writes to live relay
sessions
The wrong-identity refusal test and the auto-name retry loop still wrote
into the live pairing-trace.log on every e2e run: three bogus "restored
signer answered as a different account" npubs and phantom "auto-name
attempt" lines were test traffic, not live Amber misbehaviour, and they
kept derailing the forensics review. fail and the background enrichment
traces now check live_relays like every other trace site.
Verified: running the e2e suite appends zero lines to the trace file.
---
src/signer/nip46_client.rs | 58 +++++++++++++++++++++++++++++---------
1 file changed, 44 insertions(+), 14 deletions(-)
diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs
index 44ddf95..0d6ab3d 100644
--- a/src/signer/nip46_client.rs
+++ b/src/signer/nip46_client.rs
@@ -1305,7 +1305,22 @@ impl Nip46ClientSigner {
let message = message.into();
eprintln!("[nip46] session failed: {message}");
eprintln!("[NIP46] session failed: {message}");
- pairing_trace(&format!("session failed: {message}"));
+ // Forensics-log only LIVE sessions. The e2e harness deliberately
+ // fails restored sessions (wrong-identity refusal test), and its
+ // "session failed" lines were landing in pairing-trace.log among
+ // real ones — three different bogus "restored signer answered as a
+ // different account" npubs turned out to be test runs, not live
+ // Amber misbehaviour. Read the relays under a short lock before the
+ // mutating one below.
+ let relays = self
+ .inner
+ .try_lock()
+ .ok()
+ .and_then(|g| g.connection.as_ref().map(|c| c.relays.clone()))
+ .unwrap_or_default();
+ if live_relays(&relays) {
+ pairing_trace(&format!("session failed: {message}"));
+ }
if let Ok(mut inner) = self.inner.try_lock() {
// First failure wins: the demux loop exits with a generic
// "Connect handshake failed" AFTER the handshake task already
@@ -2328,6 +2343,7 @@ impl Nip46ClientSigner {
// UI polls status and vault, so the row fills in a moment later.
{
let app = self.app.clone();
+ let live_enrichment = live_relays(&connection.relays);
tokio::spawn(async move {
let relays_for_meta = {
let app = app.lock().await;
@@ -2358,15 +2374,27 @@ impl Nip46ClientSigner {
meta = Some(found);
break;
}
- Ok(Ok(None)) => pairing_trace(&format!(
- "auto-name attempt {attempt}: no kind-0 found on any relay"
- )),
- Ok(Err(join_err)) => pairing_trace(&format!(
- "auto-name attempt {attempt}: fetch task panicked: {join_err}"
- )),
- Err(_) => pairing_trace(&format!(
- "auto-name attempt {attempt}: fetch timed out (75s budget)"
- )),
+ Ok(Ok(None)) => {
+ if live_enrichment {
+ pairing_trace(&format!(
+ "auto-name attempt {attempt}: no kind-0 found on any relay"
+ ));
+ }
+ }
+ Ok(Err(join_err)) => {
+ if live_enrichment {
+ pairing_trace(&format!(
+ "auto-name attempt {attempt}: fetch task panicked: {join_err}"
+ ));
+ }
+ }
+ Err(_) => {
+ if live_enrichment {
+ pairing_trace(&format!(
+ "auto-name attempt {attempt}: fetch timed out (75s budget)"
+ ));
+ }
+ }
}
if attempt < 4 {
tokio::time::sleep(Duration::from_secs(20)).await;
@@ -2376,10 +2404,12 @@ impl Nip46ClientSigner {
Some(meta) => meta,
None => return,
};
- pairing_trace(&format!(
- "auto-name: kind-0 fetched (display_name={:?} name={:?})",
- meta.display_name, meta.name
- ));
+ if live_enrichment {
+ pairing_trace(&format!(
+ "auto-name: kind-0 fetched (display_name={:?} name={:?})",
+ meta.display_name, meta.name
+ ));
+ }
let mut app = app.lock().await;
let mut changed = false;
if let Some(row) = app
From 1b4655c07bbc7d9a15a730bcba4ce3638eb8d7c1 Mon Sep 17 00:00:00 2001
From: Avi
Date: Sat, 26 Sep 2026 20:37:21 -0500
Subject: [PATCH 02/17] docs(checkpoint): forensics log cleaned + live publish
confirmed at 332ab64 (2026-09-26)
---
CHECKPOINT-encryption.md | 55 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index f4cb723..e88cb10 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -1,3 +1,58 @@
+# Checkpoint — forensics log cleaned + live publish confirmed (2026-09-26)
+
+## Where things are
+- Project: `/home/avi/Projects/Keynctr`
+- Branch: `master` @ **`332ab64`** ("fix(nip46): gate remaining trace
+ writes to live relay sessions"). Previous: `704addc` (checkpoint),
+ `bc736ff` (auto-name retry), `b5c61de`, `fc2fe93`.
+- Working tree clean except the standing untracked files
+ (COSMIC_THEME.md, icon jpeg, deferred/).
+- Release binary rebuilt at 332ab64 (mtime Sep 26 20:34, real 22s
+ compile, not a cache hit).
+
+## What was completed
+1. **LIVE PUBLISH CONFIRMED (was the last open item)** — el.log shows
+ three sign_event responses ~19:50 Sep 25 and relay probes confirm
+ kind:1 notes (id 5191172d01f9…, fe9a256f597f…, text "test" +
+ image) from npub1qn0w4… accepted on primal/damus/snort/nos.lol at
+ exactly those timestamps. End-to-end Amber signing works.
+2. **False alarms retired**: the repeated "restored signer answered as
+ a different account" and duplicate "auto-name attempt" lines in
+ pairing-trace.log were E2E TEST traffic (wrong-identity refusal test
+ + loopback auto-name loop), not live Amber failures — each bogus
+ npub appeared exactly at test-run times (17:09 rebuild, 20:07 suite).
+3. **Trace gating fix (332ab64)**: `fail()` and the background
+ auto-name traces now check `live_relays()` like every other site.
+ Verified by measurement: e2e suite run leaves pairing-trace.log
+ byte-identical (was 240 lines before, 240 after).
+4. **Live vault pruned (not in git)**: dropped the legacy `fac852dc…`
+ connection row (15:37 pairing, predates client-key persistence,
+ superseded by 19:35 `4148a9a1…` pairing) so startup restore can't
+ waste a re-dial on a keyless row. Backup:
+ profiles_vault.json.backup-cron-20260926. Done with backend down.
+
+## Verified this session
+- cargo test: 216 unit + 5 e2e green. clippy --all-targets: 0 warnings.
+ cargo fmt --check clean. cargo build --release rebuilt at 332ab64.
+- Frontend untouched this session (no npm run needed).
+- Serve smoke test on the REAL vault (backend was down): startup
+ restore fires "restoring session: peer=4148a9a1… client
+ pubkey=64ea18e8…" (the persisted key from the 19:35 pairing), relays
+ connect, no errors. Full handshake needs Amber online — user test.
+- kind-0 'web5osint' confirmed live on nos.lol; profile row already
+ carries the name + picture in the vault.
+
+## Outstanding / next user steps
+- One scanless restart check: launch the GUI with Amber online and
+ watch for "identity check on restored session: PASS" without
+ scanning (restore now targets only the restorable 4148a9a1 row).
+- Optional: publish kind-0 to primal/damus too so naming doesn't
+ depend on nos.lol alone (needs one Amber signature).
+- reminder: pkill patterns matching their own launch string kill the
+ cron shell — resolve PID by full binary path first.
+
+---
+
# Checkpoint — auto-naming hardened (2026-09-25 eve)
## What changed since the label-step checkpoint
From c89b31aaf1047bc3a6484b62ea69b3e8c8de271b Mon Sep 17 00:00:00 2001
From: Avi
Date: Sun, 27 Sep 2026 21:01:23 -0500
Subject: [PATCH 03/17] =?UTF-8?q?feat(nip46):=20pair=20a=20second=20signer?=
=?UTF-8?q?=20account=20=E2=80=94=20park=20the=20live=20session,=20switch?=
=?UTF-8?q?=20re-dials=20it?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
One live NIP-46 session, many saved ones (Option A):
- start_pairing/connect while a session is live PARKS it instead of
refusing: row, pairing secret, and persisted client key stay intact,
so the parked account is restorable with no fresh scan.
- SelectProfile follows the switch: target has a restorable connection ->
park current + re-dial target's row (expected_identity guard applies);
target is local-key or unpaired -> live session untouched.
- New nip46_cancel_pairing IPC: aborts ONLY an in-flight pairing and
re-dials the parked session, so cancel-after-park is transparent.
The QR cancel paths (Add-profile modal, Signer Mode screen) use it —
plain disconnect would revoke the parked connection.
- e2e: two fake Ambers on one relay; A pairs, B's pairing parks A
(revoked_at none, client key resolvable), switch back re-dials A and
signs; no-op switch; local profile leaves session alone; B restorable.
---
frontend/src/lib/api.ts | 1 +
frontend/src/screens/CreateProfileModal.tsx | 10 +-
frontend/src/screens/SignerModeScreen.tsx | 11 +-
frontend/src/state/AppProvider.tsx | 4 +
frontend/src/test/CreateProfileModal.test.tsx | 5 +-
frontend/src/test/fakeBackend.ts | 8 +
src/ipc.rs | 79 ++++--
src/signer/nip46_client.rs | 142 ++++++++++
tests/nip46_e2e.rs | 245 ++++++++++++++++++
9 files changed, 482 insertions(+), 23 deletions(-)
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 7d6fdb5..fc67375 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -121,6 +121,7 @@ export const api = {
call('nip46_connect', { uri, label }),
nip46PairStart: (label: string) => call('nip46_pair_start', { label }),
nip46Disconnect: () => call('nip46_disconnect'),
+ nip46CancelPairing: () => call('nip46_cancel_pairing'),
nip46Status: () => call('nip46_status'),
nip46Approve: (id: string, approved: boolean, always = false) =>
call('nip46_approve', { id, approved, always }),
diff --git a/frontend/src/screens/CreateProfileModal.tsx b/frontend/src/screens/CreateProfileModal.tsx
index 7c259e5..db39ef7 100644
--- a/frontend/src/screens/CreateProfileModal.tsx
+++ b/frontend/src/screens/CreateProfileModal.tsx
@@ -15,7 +15,8 @@ interface CreateProfileModalProps {
type Phase = 'choice' | 'pairing' | 'paired' | 'local' | 'creating' | 'success';
export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
- const { state, createProfile, nip46PairStart, nip46Status, nip46Disconnect, refresh } = useApp();
+ const { state, createProfile, nip46PairStart, nip46Status, nip46CancelPairing, refresh } =
+ useApp();
const [label, setLabel] = useState('');
const [phase, setPhase] = useState('choice');
const [error, setError] = useState(null);
@@ -149,13 +150,16 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
// Leaving the QR view mid-pairing aborts the in-flight pairing; nothing
// was persisted yet, so teardown is safe at any point (same as Signer
- // Mode's "Cancel pairing").
+ // Mode's "Cancel pairing"). Cancel (not disconnect): when pairing a
+ // second signer account parked the first one, cancelling must restore
+ // the parked session rather than revoke anything.
const cancelPairing = async () => {
setLivePairingUri(null);
setPairingQr(null);
setPhase('choice');
try {
- await nip46Disconnect();
+ await nip46CancelPairing();
+ void refresh();
} catch {
// Best-effort abort; a dead pairing attempt expires on its own.
}
diff --git a/frontend/src/screens/SignerModeScreen.tsx b/frontend/src/screens/SignerModeScreen.tsx
index 2bdd00f..83dd1c7 100644
--- a/frontend/src/screens/SignerModeScreen.tsx
+++ b/frontend/src/screens/SignerModeScreen.tsx
@@ -17,6 +17,7 @@ export function SignerModeScreen() {
nip46Connect,
nip46PairStart,
nip46Disconnect,
+ nip46CancelPairing,
nip46Approve,
embeddedSignerApprove,
refresh,
@@ -192,17 +193,19 @@ export function SignerModeScreen() {
};
}, [pairingUri]);
- // Abort an in-flight pairing (e.g. expired QR) — same teardown as a
- // disconnect; nothing was persisted yet so it is safe at any point.
+ // Abort an in-flight pairing (e.g. expired QR): cancel the pairing
+ // attempt only. Never disconnect here — if pairing a second signer
+ // account parked the first one, a cancel must bring the parked session
+ // back instead of revoking it.
const handlePairCancel = useCallback(async () => {
setPairError(null);
try {
- const status = await nip46Disconnect();
+ const status = await nip46CancelPairing();
setNip46StatusState(status);
} catch (err) {
setPairError(err instanceof Error ? err.message : String(err));
}
- }, [nip46Disconnect]);
+ }, [nip46CancelPairing]);
const handleNip46Disconnect = useCallback(async () => {
setError(null);
diff --git a/frontend/src/state/AppProvider.tsx b/frontend/src/state/AppProvider.tsx
index 543620c..9aa5483 100644
--- a/frontend/src/state/AppProvider.tsx
+++ b/frontend/src/state/AppProvider.tsx
@@ -82,6 +82,7 @@ interface AppContextValue {
nip46Connect: (uri: string, label: string) => Promise;
nip46PairStart: (label: string) => Promise;
nip46Disconnect: () => Promise;
+ nip46CancelPairing: () => Promise;
nip46Status: () => Promise;
nip46Approve: (id: string, approved: boolean, always?: boolean) => Promise;
// Legacy NIP-46 bunker (deprecated)
@@ -289,6 +290,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
[],
);
const nip46Disconnect = useCallback(() => api.nip46Disconnect(), []);
+ const nip46CancelPairing = useCallback(() => api.nip46CancelPairing(), []);
const nip46Status = useCallback(() => api.nip46Status(), []);
const nip46PairStart = useCallback((label: string) => api.nip46PairStart(label), []);
const nip46Approve = useCallback(
@@ -385,6 +387,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
nip46Connect,
nip46PairStart,
nip46Disconnect,
+ nip46CancelPairing,
nip46Status,
nip46Approve,
signerConnect,
@@ -445,6 +448,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
nip46Connect,
nip46PairStart,
nip46Disconnect,
+ nip46CancelPairing,
nip46Status,
nip46Approve,
signerConnect,
diff --git a/frontend/src/test/CreateProfileModal.test.tsx b/frontend/src/test/CreateProfileModal.test.tsx
index da456cb..a4d3123 100644
--- a/frontend/src/test/CreateProfileModal.test.tsx
+++ b/frontend/src/test/CreateProfileModal.test.tsx
@@ -137,7 +137,10 @@ describe('CreateProfileModal', () => {
await screen.findByText(/Waiting for the signer to scan/i);
await user.click(screen.getByRole('button', { name: 'Cancel pairing' }));
- expect(backend.requests.some((r) => r.method === 'nip46_disconnect')).toBe(true);
+ // Cancel must be the NON-revoking cancel (parked sessions survive it),
+ // never the disconnect that revokes the stored connection.
+ expect(backend.requests.some((r) => r.method === 'nip46_cancel_pairing')).toBe(true);
+ expect(backend.requests.some((r) => r.method === 'nip46_disconnect')).toBe(false);
expect(
screen.getByRole('button', { name: /Sign in with a signer app \(Amber\)/ }),
).toBeInTheDocument();
diff --git a/frontend/src/test/fakeBackend.ts b/frontend/src/test/fakeBackend.ts
index 47e6954..40b147c 100644
--- a/frontend/src/test/fakeBackend.ts
+++ b/frontend/src/test/fakeBackend.ts
@@ -226,6 +226,14 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
backend.setNip46(next);
return next;
}
+ case 'nip46_cancel_pairing': {
+ // Mirrors the real backend: aborts ONLY the pairing attempt and
+ // re-dials the parked session — a cancel must not clear a
+ // connected session, only the pairing URI.
+ const next = { ...backend.nip46, pairing_uri: undefined };
+ backend.setNip46(next);
+ return next;
+ }
case 'nip46_approve':
return backend.nip46;
diff --git a/src/ipc.rs b/src/ipc.rs
index 5ed59d5..93f8330 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -173,6 +173,12 @@ pub enum Request {
},
/// Disconnect from the NIP-46 signer.
Nip46Disconnect,
+ /// Cancel an in-flight pairing (the GUI left the QR view): abort the
+ /// pairing attempt WITHOUT touching any parked/saved session, then try
+ /// to re-dial the active profile's saved session so parking for a
+ /// cancelled pairing is fully transparent. (Plain Nip46Disconnect would
+ /// revoke the currently stored connection — wrong for a cancel.)
+ Nip46CancelPairing,
/// Get NIP-46 connection status.
Nip46Status,
/// Approve/reject a pending NIP-46 request. `always = true` additionally
@@ -492,6 +498,13 @@ async fn run(app: &Arc>, request: Request) -> Result>, request: Request) -> Result>, request: Request) -> Result {
+ let Some(signer) = ensure_nip46_signer(app).await else {
+ return Err(AppError::config("NIP-46 signer not initialized"));
+ };
+ signer.cancel_pairing().await?;
+ let status = signer.status().await;
+ Ok(json!(status))
+ }
Request::Nip46Status => {
let Some(signer) = ensure_nip46_signer(app).await else {
return Ok(json!({ "connected": false, "error": "Not initialized" }));
@@ -654,6 +681,41 @@ async fn run(app: &Arc>, request: Request) -> Result {
+ {
+ let mut guard = app.lock().await;
+ profiles::set_active(&mut guard.vault, &npub)?;
+ if guard.signer_mode == SignerMode::Embedded {
+ if let Some(signer) = &guard.embedded_signer {
+ signer.set_active_profile(Some(npub.clone())).await;
+ }
+ } else if guard.signer_mode == SignerMode::Nip46Client {
+ if let Some(signer) = &guard.nip46_signer {
+ signer.set_active_profile(Some(npub.clone())).await;
+ }
+ }
+ guard.save_vault()?;
+ }
+ // Option A: follow the switch with the signer session. If the
+ // target profile has a restorable NIP-46 connection, the live
+ // session (if any, serving a different account) is parked and
+ // this profile's session is re-dialed — no fresh scan. If the
+ // target has no signer connection (local-key profile), the live
+ // session is left alone.
+ if app.lock().await.signer_mode == SignerMode::Nip46Client {
+ if let Some(signer) = ensure_nip46_signer(app).await {
+ if let Err(e) = signer.switch_to_profile(&npub).await {
+ eprintln!("[NIP46] profile switch session change failed: {e}");
+ }
+ }
+ }
+ let guard = app.lock().await;
+ Ok(json!(guard.state_view()))
+ }
+
// Vault state requests (require lock)
other => {
let mut guard = app.lock().await;
@@ -711,21 +773,8 @@ async fn run_with_app(app: &mut App, request: Request) -> Result {
- profiles::set_active(&mut app.vault, &npub)?;
- if app.signer_mode == SignerMode::Embedded {
- if let Some(signer) = &app.embedded_signer {
- signer.set_active_profile(Some(npub)).await;
- }
- } else if app.signer_mode == SignerMode::Nip46Client {
- if let Some(signer) = &app.nip46_signer {
- signer.set_active_profile(Some(npub)).await;
- }
- }
- app.save_vault()?;
- Ok(json!(app.state_view()))
- }
-
+ // NOTE: SelectProfile is handled in `run` (above), not here — it
+ // must drop the App guard before switching the signer session.
Request::PublishProfileMetadata { npub } => {
// Route through the profile's Signing source, exactly like
// PublishNote: an embedded profile signs locally, a paired
diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs
index 0d6ab3d..1248714 100644
--- a/src/signer/nip46_client.rs
+++ b/src/signer/nip46_client.rs
@@ -488,6 +488,148 @@ impl Nip46ClientSigner {
self.disconnect().await
}
+ /// Park the live session: stop the wire task and clear in-memory state,
+ /// but keep the vault row, pairing secret, and persisted client key
+ /// INTACT, so the session can be re-dialed later with no fresh scan.
+ ///
+ /// Unlike [`Self::disconnect`] this does NOT revoke: switching signer
+ /// accounts (pair a second Amber account, or switch back to a previously
+ /// paired one) must leave the parked account restorable. The vault row
+ /// is what `reactivate_saved_sessions` dials from, so a parked session
+ /// is exactly a saved session.
+ pub async fn park_live_session(&self) {
+ let mut inner = self.inner.lock().await;
+ if let Some(task) = inner.task.take() {
+ task.abort();
+ }
+ if let Some(pairing) = inner.pairing.take() {
+ pairing.task.abort();
+ }
+ if let Some(client) = inner.client.take() {
+ let _ = client.disconnect().await;
+ }
+ inner.phase = Nip46Phase::Stopped;
+ inner.connection = None;
+ inner.conversation_key = None;
+ inner.keys = None;
+ inner.identity = None;
+ inner.expected_identity = None;
+ inner.active_npub = None;
+ inner.pending.clear();
+ // Wake any callers awaiting a remote response; their waiters turn
+ // into `NotConnected` rather than hanging until the request timeout.
+ for (_, waiter) in inner.remote_pending.drain() {
+ let _ = waiter.sender.send(Err(
+ "Switched signer accounts before the signer responded.".to_string(),
+ ));
+ }
+ }
+
+ /// Whether a session or pairing is currently live.
+ pub async fn has_live_session(&self) -> bool {
+ let inner = self.inner.lock().await;
+ inner.task.is_some() || inner.pairing.is_some()
+ }
+
+ /// Cancel an in-flight pairing attempt: abort ONLY the pairing task and
+ /// its session state, then try to re-dial the active profile's saved
+ /// session. Used by the GUI when the user leaves the QR view after
+ /// pairing-for-a-second-account parked the first one: the cancel must
+ /// not revoke anything, and the parked session should come back so the
+ /// park is invisible.
+ pub async fn cancel_pairing(&self) -> Result<(), AppError> {
+ {
+ let mut inner = self.inner.lock().await;
+ if let Some(pairing) = inner.pairing.take() {
+ pairing.task.abort();
+ }
+ // A pairing that was cancelled before anyone scanned never
+ // reached the vault; the only live slot it held is now free.
+ // If a *connected* session exists (task, not pairing), leave it
+ // alone — cancelling pairing must not kill a working session.
+ if inner.task.is_some() {
+ return Ok(());
+ }
+ inner.phase = Nip46Phase::Stopped;
+ inner.connection = None;
+ inner.conversation_key = None;
+ inner.keys = None;
+ inner.identity = None;
+ inner.expected_identity = None;
+ inner.active_npub = None;
+ inner.pending.clear();
+ }
+ // The park-then-pair flow (Option A) may have left the previous
+ // account parked: re-dial it (reactivate prefers the active
+ // profile's row) so the cancelled pairing changes nothing.
+ self.reactivate_saved_sessions().await?;
+ Ok(())
+ }
+
+ /// Follow a profile switch with the signer session (Option A: one live
+ /// session, many saved ones).
+ ///
+ /// - The live session already serves `npub`: keep it, do nothing.
+ /// - `npub` has no live saved NIP-46 connection (local-key profile, or
+ /// never paired): leave the live session alone — signing for `npub`
+ /// routes through its own source and killing a working Amber session
+ /// to look at a local profile would be a regression.
+ /// - `npub` has a restorable saved connection: park the live session
+ /// (restorable, not revoked) and re-dial `npub`'s row.
+ ///
+ /// Returns `true` when a re-dial was started. The re-dial resolves
+ /// asynchronously through the same handshake as restore, including the
+ /// `expected_identity` cross-account guard.
+ pub async fn switch_to_profile(&self, npub: &str) -> Result {
+ // Already serving the target identity? Nothing to do.
+ {
+ let inner = self.inner.lock().await;
+ if let Some(identity) = &inner.identity {
+ if identity.to_bech32().ok().as_deref() == Some(npub) {
+ return Ok(false);
+ }
+ }
+ }
+
+ // Does the target profile have a live, restorable saved connection?
+ let restorable = {
+ let app = self.app.lock().await;
+ let vault_key = app.vault_key().copied();
+ let now = crate::vault::unix_timestamp().unwrap_or(0);
+ app.vault.nip46_connections.iter().any(|c| {
+ c.profile_npub.as_deref() == Some(npub)
+ && c.revoked_at.is_none()
+ && c.expires_at.map(|t| t > now).unwrap_or(true)
+ && crate::vault::resolve_connection_client_key(
+ &app.vault,
+ vault_key.as_ref(),
+ &crate::signer::VaultRef::from_connection(c),
+ )
+ .ok()
+ .flatten()
+ .is_some()
+ })
+ };
+ if !restorable {
+ return Ok(false);
+ }
+
+ // Make sure the vault agrees on the target before re-dialing (the
+ // IPC path already did this; idempotent here, and it makes the
+ // reactivate preference order correct regardless of caller).
+ {
+ let mut app = self.app.lock().await;
+ if app.vault.active_profile.as_deref() != Some(npub) {
+ crate::profiles::set_active(&mut app.vault, npub)?;
+ app.save_vault()?;
+ }
+ }
+
+ self.park_live_session().await;
+ let dialed = self.reactivate_saved_sessions().await?;
+ Ok(dialed > 0)
+ }
+
/// Re-dial the saved signer sessions after startup/unlock, no scan.
///
/// Amber remembers our *client pubkey* as the identity of an approved
diff --git a/tests/nip46_e2e.rs b/tests/nip46_e2e.rs
index 86b281b..0737396 100644
--- a/tests/nip46_e2e.rs
+++ b/tests/nip46_e2e.rs
@@ -1324,3 +1324,248 @@ async fn nip46_session_restore_redials_and_refuses_wrong_identity() {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
+
+// ---------------------------------------------------------------------------
+// Option A: many saved sessions, one live. Pairing/connecting a second
+// signer account PARKS the live session (restorable, never revoked), and
+// switching a profile back to a parked account re-dials it with no scan.
+// ---------------------------------------------------------------------------
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+#[allow(clippy::await_holding_lock)]
+async fn nip46_second_account_parks_first_and_switch_restores_it() {
+ let _vault_guard = VAULT_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
+ let app = {
+ let tmp = std::env::temp_dir().join(format!(
+ "keynectr-e2e-switch-{}-{}",
+ std::process::id(),
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ let app_dir = tmp.join("keynectr");
+ std::fs::create_dir_all(&app_dir).unwrap();
+ std::fs::write(
+ app_dir.join("profiles_vault.json"),
+ serde_json::to_string(&Vault::empty()).unwrap(),
+ )
+ .unwrap();
+ std::env::set_var("XDG_DATA_HOME", &tmp);
+ let app = std::sync::Arc::new(Mutex::new(App::load().expect("load app")));
+ assert!(
+ app.try_lock().unwrap().vault.profiles.is_empty(),
+ "e2e vault isolation failed for switch test"
+ );
+ app
+ };
+
+ let relay_url = start_relay().await;
+ let comms_a = Keys::generate();
+ let identity_a = Keys::generate();
+ let comms_b = Keys::generate();
+ let identity_b = Keys::generate();
+ // Two fake Ambers on one relay: each only answers traffic it can
+ // NIP-44-decrypt with its own comms key, so they never cross-talk.
+ tokio::spawn(run_fake_amber(
+ relay_url.clone(),
+ comms_a.clone(),
+ identity_a.clone(),
+ Duration::from_millis(30),
+ ));
+ tokio::spawn(run_fake_amber(
+ relay_url.clone(),
+ comms_b.clone(),
+ identity_b.clone(),
+ Duration::from_millis(30),
+ ));
+
+ let signer = Nip46ClientSigner::new(app.clone());
+
+ let wait_connected = |signer: Nip46ClientSigner| async move {
+ let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
+ loop {
+ let st = signer.status().await;
+ if let Some(err) = &st.error {
+ panic!("session failed: {err}");
+ }
+ if st.connected {
+ return;
+ }
+ assert!(
+ tokio::time::Instant::now() < deadline,
+ "session never connected: {:?}",
+ signer.status().await
+ );
+ tokio::time::sleep(Duration::from_millis(100)).await;
+ }
+ };
+
+ // --- 1. Account A pairs (the flow the GUI runs).
+ signer
+ .connect(
+ &format!(
+ "bunker://{}?relay={}",
+ comms_a.public_key().to_hex(),
+ relay_url
+ ),
+ "amber A".to_string(),
+ )
+ .await
+ .expect("connect account A");
+ wait_connected(signer.clone()).await;
+ let npub_a = SignerTrait::get_public_key(&signer)
+ .await
+ .expect("identity A")
+ .to_bech32()
+ .unwrap();
+
+ // --- 2. Account B pairs while A is live. The IPC dispatcher parks the
+ // live session first (the exact sequence the dispatcher now runs).
+ assert!(signer.has_live_session().await);
+ signer.park_live_session().await;
+ assert!(
+ !signer.has_live_session().await,
+ "park must clear the live slot"
+ );
+ signer
+ .connect(
+ &format!(
+ "bunker://{}?relay={}",
+ comms_b.public_key().to_hex(),
+ relay_url
+ ),
+ "amber B".to_string(),
+ )
+ .await
+ .expect("connect account B while A is parked");
+ wait_connected(signer.clone()).await;
+ let npub_b = SignerTrait::get_public_key(&signer)
+ .await
+ .expect("identity B")
+ .to_bech32()
+ .unwrap();
+ assert_ne!(npub_a, npub_b, "two accounts, two identities");
+
+ // B is fully usable: sign through it.
+ let unsigned = UnsignedEvent::new(
+ identity_b.public_key(),
+ Timestamp::now(),
+ Kind::TextNote,
+ vec![],
+ "signed by B".to_string(),
+ );
+ let signed = SignerTrait::sign_event(&signer, unsigned.clone())
+ .await
+ .expect("sign through B");
+ assert!(signed.verify_signature());
+
+ // Parking must NOT have revoked A: its row stays live with its client
+ // key resolvable — that is what makes it restorable.
+ let ref_a =
+ keynectr::signer::VaultRef::new(Some(npub_a.clone()), comms_a.public_key().to_hex());
+ {
+ let g = app.lock().await;
+ let row = g
+ .vault
+ .nip46_connections
+ .iter()
+ .find(|c| c.profile_npub.as_deref() == Some(npub_a.as_str()))
+ .expect("A's connection row survives B's pairing");
+ assert!(
+ row.revoked_at.is_none(),
+ "parked session must not be revoked"
+ );
+ assert!(
+ keynectr::vault::resolve_connection_client_key(&g.vault, None, &ref_a)
+ .expect("resolve")
+ .is_some(),
+ "parked session must keep its client key"
+ );
+ }
+
+ // --- 3. Switch back to A: park B, re-dial A — no fresh pairing.
+ let dialed = signer
+ .switch_to_profile(&npub_a)
+ .await
+ .expect("switch to A");
+ assert!(dialed, "A has a restorable session, switch must re-dial it");
+ wait_connected(signer.clone()).await;
+ let back = SignerTrait::get_public_key(&signer)
+ .await
+ .expect("identity after switch");
+ assert_eq!(
+ back.to_bech32().unwrap(),
+ npub_a,
+ "switched session must answer as A"
+ );
+ let unsigned_a = UnsignedEvent::new(
+ identity_a.public_key(),
+ Timestamp::now(),
+ Kind::TextNote,
+ vec![],
+ "signed by A again".to_string(),
+ );
+ let signed_a = SignerTrait::sign_event(&signer, unsigned_a.clone())
+ .await
+ .expect("sign through A after switch");
+ assert!(signed_a.verify_signature());
+ assert_eq!(signed_a.content, "signed by A again");
+
+ // Switching to A again is a no-op (already serving A): no second dial.
+ assert!(
+ !signer
+ .switch_to_profile(&npub_a)
+ .await
+ .expect("noop switch"),
+ "switch to the identity already live must not re-dial"
+ );
+
+ // --- 4. A profile with NO signer connection must leave B... (here A)
+ // alone: local-key profiles route signing through their own source.
+ let local = Keys::generate();
+ let npub_local = local.public_key().to_bech32().unwrap();
+ {
+ let mut g = app.lock().await;
+ g.vault.profiles.push(keynectr::vault::StoredProfile {
+ label: "local".to_string(),
+ public_key: npub_local.clone(),
+ secret_key: local
+ .secret_key()
+ .to_secret_bytes()
+ .iter()
+ .map(|b| format!("{b:02x}"))
+ .collect(),
+ created_at: 0,
+ picture: None,
+ nip05: None,
+ signer_mode: keynectr::vault::SignerMode::Embedded,
+ });
+ g.save_vault().unwrap();
+ }
+ assert!(
+ !signer
+ .switch_to_profile(&npub_local)
+ .await
+ .expect("switch to local"),
+ "local-key profile must not touch the live signer session"
+ );
+ assert!(
+ signer.status().await.connected,
+ "live A session survives a switch to a local profile"
+ );
+ let still_a = SignerTrait::get_public_key(&signer).await.expect("still A");
+ assert_eq!(still_a.to_bech32().unwrap(), npub_a);
+
+ // And switching back to B works too — B was parked, never revoked.
+ let dialed_b = signer
+ .switch_to_profile(&npub_b)
+ .await
+ .expect("switch back to B");
+ assert!(dialed_b, "B was parked, must be restorable");
+ wait_connected(signer.clone()).await;
+ let b_again = SignerTrait::get_public_key(&signer).await.expect("B again");
+ assert_eq!(b_again.to_bech32().unwrap(), npub_b);
+
+ signer.disconnect().await.ok();
+}
From 98593cb1704651858a5eae685a64f29141a7f501 Mon Sep 17 00:00:00 2001
From: Avi
Date: Sun, 27 Sep 2026 21:02:21 -0500
Subject: [PATCH 04/17] docs(checkpoint): multi-account signer switching
(park/switch/cancel) at c89b31a (2026-09-27)
---
CHECKPOINT-encryption.md | 59 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index e88cb10..9f3d5d9 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -1,3 +1,62 @@
+# Checkpoint — multi-account signer switching (Option A) (2026-09-27 eve)
+
+## Where things are
+- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`c89b31a`**
+ ("feat(nip46): pair a second signer account — park the live session,
+ switch re-dials it"). Previous: `1b4655c` (checkpoint), `332ab64`.
+- Working tree: clean for tracked files. Untracked intentionally NOT
+ committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
+ `deferred/`.
+
+## What was completed (user-facing)
+1. **You can now add a second Amber account.** Pairing a new signer while
+ one is connected no longer says "Already connected — disconnect
+ first": the current session is PARKED (kept restorable, never revoked)
+ and the new account pairs.
+2. **Switching profiles switches signer accounts.** Click a profile in
+ Profiles: if it has a saved signer session, the live one is parked and
+ that profile's session is re-dialed automatically (no scan, identity
+ guard still enforced). Local-key profiles leave the signer alone.
+3. **Cancel on the QR is safe.** Leaving the QR view cancels only the
+ pairing attempt and brings the parked session back (new
+ `nip46_cancel_pairing` IPC; the old path revoked).
+- One session is live at a time (Amber signs one active account anyway);
+ all others stay saved and switchable.
+
+## Commits added
+- `c89b31a` feat(nip46): pair a second signer account — park the live session, switch re-dials it
+
+## Verification (all green at c89b31a)
+- Rust: `cargo test` 216 unit + 6 e2e (NEW: two fake Ambers on one relay —
+ B's pairing parks A unrevoked with client key intact; switch back
+ re-dials A and signs; no-op switch; local profile untouched; B
+ restorable). `cargo clippy --all-targets` 0 warnings; `cargo fmt --check`
+ clean; `cargo build --release` rebuilt (binary mtime Sep 27 21:00).
+- Frontend: `npm test` 125 passed; `typecheck`, `lint`, `format:check`
+ clean; `npm run build` + `electron:build` green.
+
+## Resume / reproduce
+- GUI: relaunch the app (or restart the backend) to pick up the new
+ release binary. Profiles -> click another paired profile -> log shows
+ `restoring session` + `identity check on restored session: PASS`.
+- Add account B: Create Profile -> Sign in with Amber -> switch to
+ account B IN AMBER -> scan. Account A stays restorable.
+- e2e: `cargo test --test nip46_e2e` (6 tests, loopback relay only).
+
+## Outstanding
+- LIVE two-account eyeball by the user (pair account B from a second
+ Amber profile, switch back and forth) — e2e proves the mechanics, a
+ real-device pass confirms it end-to-end.
+- Live "Check for updates" failure is an ENVIRONMENT issue, not a bug:
+ the updater shells out to `npm outdated`/`cargo update` in the source
+ tree; the spawned backend's PATH lacks npm/cargo (Electron-launched
+ process), so it errors. Fix options: bake a login-shell PATH into the
+ updater or surface a clearer message. Not started.
+- Publish kind-0 to primal/damus (one Amber approval); Step 4
+ permissions UI; KDF upgrade (Step 5); rename pass (Step 7).
+
+---
+
# Checkpoint — forensics log cleaned + live publish confirmed (2026-09-26)
## Where things are
From adbc7c2d7587a27d98ee7d99ef15f2c139151b4a Mon Sep 17 00:00:00 2001
From: Avi
Date: Sun, 27 Sep 2026 22:37:54 -0500
Subject: [PATCH 05/17] =?UTF-8?q?feat(signer):=20permissions=20UI=20?=
=?UTF-8?q?=E2=80=94=20declared=20grants=20surfaced,=20always-allow=20kind?=
=?UTF-8?q?-scoped?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Step 4 groundwork, the enforcement half that was invisible or too broad:
- Nip46Status now carries the connection's declared perms= grant list and
its expiry; Signer Mode shows a Permissions panel on a live session
(explicit grant rows, or a plain statement that the signer app approves
each request when no list was declared).
- 'Always allow' grants are kind-scoped: a sign_event grant records the
kind of the request the user actually approved and never covers other
kinds. Legacy kind-less grants keep their all-kinds meaning so existing
vaults keep working. Enforced in both bunker.rs and nip46_client.rs.
- Grants list on the Signer screen renders human labels with kind scope.
Tests: vault kind-scoping unit tests, frontend permission-label unit
tests + two SignerModeScreen tests (declared list, signer-side note).
---
frontend/src/lib/permissions.ts | 52 ++++++++++++
frontend/src/lib/types.ts | 20 +++++
frontend/src/screens/SignerModeScreen.tsx | 35 ++++++++
frontend/src/screens/SignerScreen.tsx | 3 +-
frontend/src/styles.css | 20 +++++
frontend/src/test/SignerModeScreen.test.tsx | 36 ++++++++
frontend/src/test/permissions.test.ts | 53 ++++++++++++
src/bunker.rs | 20 ++++-
src/signer/nip46_client.rs | 41 ++++++----
src/signer/types.rs | 10 +++
src/vault.rs | 91 +++++++++++++++++----
11 files changed, 350 insertions(+), 31 deletions(-)
create mode 100644 frontend/src/lib/permissions.ts
create mode 100644 frontend/src/test/permissions.test.ts
diff --git a/frontend/src/lib/permissions.ts b/frontend/src/lib/permissions.ts
new file mode 100644
index 0000000..aed1d04
--- /dev/null
+++ b/frontend/src/lib/permissions.ts
@@ -0,0 +1,52 @@
+import type { Nip46Permissions, SignerGrant } from './types';
+
+/** Human label for a NIP-46 method name. */
+export function methodLabel(method: string): string {
+ switch (method) {
+ case 'sign_event':
+ return 'Sign events';
+ case 'nip44_encrypt':
+ return 'Encrypt messages (NIP-44)';
+ case 'nip44_decrypt':
+ return 'Decrypt messages (NIP-44)';
+ case 'get_public_key':
+ return 'Read your public key';
+ case 'get_relays':
+ return 'Read your relay list';
+ default:
+ return method;
+ }
+}
+
+/** One-line description of a permission grant, e.g.
+ * "Sign events (kinds 1, 30023)" or "Sign events (all kinds)". */
+export function permissionLabel(method: string, allowedKinds?: number[]): string {
+ const base = methodLabel(method);
+ if (method === 'sign_event') {
+ if (!allowedKinds || allowedKinds.length === 0) return `${base} — all kinds`;
+ return `${base} — kinds ${allowedKinds.join(', ')}`;
+ }
+ return base;
+}
+
+/** Grant rows for the "always allow" list. */
+export function grantLabel(grant: SignerGrant): string {
+ return permissionLabel(grant.method, grant.allowed_kinds);
+}
+
+/** Rows for the declared per-connection permission set. An absent set means
+ * there is no local grant list — the signer app approves each request. */
+export function declaredPermissionRows(permissions?: Nip46Permissions): string[] | null {
+ if (!permissions) return null;
+ const granted = permissions.granted ?? [];
+ if (granted.length === 0) return [];
+ return granted.map((p) => permissionLabel(p.method, p.allowed_kinds));
+}
+
+/** Format a connection expiry for display. */
+export function formatExpiry(expiresAt?: number): string | null {
+ if (!expiresAt) return null;
+ const date = new Date(expiresAt * 1000);
+ if (Number.isNaN(date.getTime())) return null;
+ return date.toLocaleString();
+}
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index d0f98ab..a255dd9 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -29,6 +29,19 @@ export interface PendingApproval {
details?: ApprovalDetails;
}
+/** A single granted NIP-46 permission (mirrors the Rust Nip46Permission). */
+export interface Nip46Permission {
+ /** The NIP-46 method this covers, e.g. `sign_event`. */
+ method: string;
+ /** Event-kind restrictions for `sign_event`; empty = all kinds. */
+ allowed_kinds?: number[];
+}
+
+/** Declared per-connection permission set (from a `perms=` connect URI). */
+export interface Nip46Permissions {
+ granted?: Nip46Permission[];
+}
+
/** A standing "always allow" grant: one app may use one method without a
* prompt. Created by choosing "Always allow" on an approval; revoked from
* the Signer screen. */
@@ -37,6 +50,8 @@ export interface SignerGrant {
app_pubkey: string;
/** NIP-46 method that runs without prompting (e.g. "sign_event"). */
method: string;
+ /** Event kinds covered for `sign_event`; empty = all kinds (legacy). */
+ allowed_kinds?: number[];
}
/** Non-secret snapshot of the NIP-46 remote signer for display. */
@@ -75,6 +90,11 @@ export interface Nip46SignerStatus {
pending_approvals: PendingApproval[];
/** nostrconnect:// pairing token while a QR pairing is in flight. */
pairing_uri?: string;
+ /** Declared per-connection permissions, when the connect URI carried a
+ * `perms=` grant list. Absent = the signer app enforces via its prompts. */
+ permissions?: Nip46Permissions;
+ /** Unix timestamp when the connection expires, if it has a deadline. */
+ expires_at?: number;
}
/** Union of all signer statuses. */
diff --git a/frontend/src/screens/SignerModeScreen.tsx b/frontend/src/screens/SignerModeScreen.tsx
index 83dd1c7..51a46c8 100644
--- a/frontend/src/screens/SignerModeScreen.tsx
+++ b/frontend/src/screens/SignerModeScreen.tsx
@@ -5,6 +5,7 @@ import { Badge } from '../components/Badge';
import { Button } from '../components/Button';
import { ErrorText } from '../components/ErrorText';
import { Icon } from '../components/Icon';
+import { declaredPermissionRows, formatExpiry } from '../lib/permissions';
import type { SignerMode, EmbeddedSignerStatus, Nip46SignerStatus } from '../lib/types';
import { useApp } from '../state/AppProvider';
@@ -528,6 +529,40 @@ export function SignerModeScreen() {
via {nip46StatusState.connected_relays?.length ?? 0} of{' '}
{nip46StatusState.relays?.length ?? 0} relays
+
+
Permissions
+ {(() => {
+ const rows = declaredPermissionRows(nip46StatusState.permissions);
+ const expiry = formatExpiry(nip46StatusState.expires_at);
+ return (
+ <>
+ {rows === null ? (
+
+ This signer approves every request on your phone — Keynctr holds no
+ standing permission list for this connection.
+
+ ) : rows.length === 0 ? (
+
+ No operations were granted by the connect request.
+
+ ) : (
+
+ {rows.map((row) => (
+
+ {row}
+
+ ))}
+
+ )}
+ {expiry && (
+
+ This connection expires {expiry}.
+
+ )}
+ >
+ );
+ })()}
+
{nip46StatusState.error && (
{nip46StatusState.error}
diff --git a/frontend/src/screens/SignerScreen.tsx b/frontend/src/screens/SignerScreen.tsx
index b55d897..94a1fe3 100644
--- a/frontend/src/screens/SignerScreen.tsx
+++ b/frontend/src/screens/SignerScreen.tsx
@@ -5,6 +5,7 @@ import { Button } from '../components/Button';
import { ErrorText } from '../components/ErrorText';
import { Icon } from '../components/Icon';
import { shortHexId } from '../lib/format';
+import { grantLabel } from '../lib/permissions';
import type { SignerGrant, SignerStatus } from '../lib/types';
import { useApp } from '../state/AppProvider';
@@ -252,7 +253,7 @@ export function SignerScreen() {
{grants.map((grant) => (
-
{grant.method}
+
{grantLabel(grant)}
for {shortHexId(grant.app_pubkey)}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 3d1f373..5c3c1d6 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -2242,6 +2242,26 @@ select {
gap: 12px;
}
+.signer-permissions {
+ width: 100%;
+ padding: 10px 12px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+}
+
+.signer-permissions h3 {
+ margin: 0 0 6px;
+ font-size: 13px;
+}
+
+.signer-permission-list {
+ margin: 0;
+ padding-left: 18px;
+ font-size: 12px;
+ line-height: 1.7;
+}
+
/* -------------------------------------------------------------------------
Motion system
Purposeful motion for feedback, state, and continuity.
diff --git a/frontend/src/test/SignerModeScreen.test.tsx b/frontend/src/test/SignerModeScreen.test.tsx
index a4a94f4..f62b51d 100644
--- a/frontend/src/test/SignerModeScreen.test.tsx
+++ b/frontend/src/test/SignerModeScreen.test.tsx
@@ -69,4 +69,40 @@ describe('SignerModeScreen handshake states', () => {
expect(backend.requests.some((r) => r.method === 'nip46_status')).toBe(true),
);
});
+
+ it('shows the declared permission list on a connected session', async () => {
+ const backend = installNip46Backend();
+ backend.setNip46({
+ type: 'nip46',
+ connected: true,
+ signer_pubkey: 'aabbccddeeff0011',
+ relays: ['wss://relay.test'],
+ connected_relays: ['wss://relay.test'],
+ pending_approvals: [],
+ permissions: {
+ granted: [{ method: 'sign_event', allowed_kinds: [1, 30023] }, { method: 'nip44_encrypt' }],
+ },
+ });
+ renderWithApp(
);
+
+ expect(await screen.findByText('Permissions')).toBeInTheDocument();
+ expect(screen.getByText('Sign events — kinds 1, 30023')).toBeInTheDocument();
+ expect(screen.getByText('Encrypt messages (NIP-44)')).toBeInTheDocument();
+ });
+
+ it('explains signer-side enforcement when no grant list was declared', async () => {
+ const backend = installNip46Backend();
+ backend.setNip46({
+ type: 'nip46',
+ connected: true,
+ signer_pubkey: 'aabbccddeeff0011',
+ relays: ['wss://relay.test'],
+ connected_relays: ['wss://relay.test'],
+ pending_approvals: [],
+ });
+ renderWithApp(
);
+
+ expect(await screen.findByText('Permissions')).toBeInTheDocument();
+ expect(await screen.findByText(/approves every request on your phone/i)).toBeInTheDocument();
+ });
});
diff --git a/frontend/src/test/permissions.test.ts b/frontend/src/test/permissions.test.ts
new file mode 100644
index 0000000..d5d6f08
--- /dev/null
+++ b/frontend/src/test/permissions.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest';
+import {
+ declaredPermissionRows,
+ formatExpiry,
+ grantLabel,
+ permissionLabel,
+} from '../lib/permissions';
+
+describe('permission labels', () => {
+ it('labels a kind-scoped sign_event grant', () => {
+ expect(permissionLabel('sign_event', [1, 30023])).toBe('Sign events — kinds 1, 30023');
+ });
+
+ it('labels an all-kinds sign_event grant', () => {
+ expect(permissionLabel('sign_event', [])).toBe('Sign events — all kinds');
+ expect(permissionLabel('sign_event')).toBe('Sign events — all kinds');
+ });
+
+ it('labels non-signing methods without kind noise', () => {
+ expect(permissionLabel('nip44_decrypt')).toBe('Decrypt messages (NIP-44)');
+ });
+
+ it('renders a grant row through grantLabel', () => {
+ expect(grantLabel({ app_pubkey: 'aa', method: 'sign_event', allowed_kinds: [1] })).toBe(
+ 'Sign events — kinds 1',
+ );
+ });
+});
+
+describe('declared permission rows', () => {
+ it('returns null when the connection declared no grant list', () => {
+ expect(declaredPermissionRows(undefined)).toBeNull();
+ expect(declaredPermissionRows({})).toEqual([]);
+ });
+
+ it('renders each granted method', () => {
+ expect(
+ declaredPermissionRows({
+ granted: [{ method: 'sign_event', allowed_kinds: [1] }, { method: 'nip44_encrypt' }],
+ }),
+ ).toEqual(['Sign events — kinds 1', 'Encrypt messages (NIP-44)']);
+ });
+});
+
+describe('formatExpiry', () => {
+ it('formats a unix timestamp', () => {
+ expect(formatExpiry(1760000000)).toBeTruthy();
+ });
+
+ it('returns null when there is no deadline', () => {
+ expect(formatExpiry(undefined)).toBeNull();
+ });
+});
diff --git a/src/bunker.rs b/src/bunker.rs
index 3f2b47b..a908b25 100644
--- a/src/bunker.rs
+++ b/src/bunker.rs
@@ -402,6 +402,23 @@ struct RawRequest {
params: Vec
,
}
+/// The event kind a gated request operates on, when it has one.
+/// `sign_event` carries the unsigned event JSON in `params[0]`; other
+/// gated methods (encrypt/decrypt) have no kind dimension, and an
+/// unparseable payload returns `None` so grant checks fail closed toward
+/// prompting.
+fn request_kind(request: &RawRequest) -> Option {
+ if request.method != "sign_event" {
+ return None;
+ }
+ request
+ .params
+ .first()
+ .and_then(|json| serde_json::from_str::(json).ok())
+ .and_then(|v| v.get("kind").and_then(|k| k.as_u64()))
+ .map(|k| k as u16)
+}
+
/// `{"id":..,"result":,"error":null}`
fn response_ok(id: &str, result: String) -> String {
json!({ "id": id, "result": result, "error": null }).to_string()
@@ -754,11 +771,12 @@ async fn run_sign_task(signer: Signer, app: Arc>, uri: C
// run — unless the user granted this app standing "always allow"
// permission for that method. Everything else is answered immediately.
let response = if requires_approval(&request.method) {
+ let kind = request_kind(&request);
let granted = {
let guard = app.lock().await;
guard
.vault
- .has_signer_grant(&uri.peer.to_hex(), &request.method)
+ .has_signer_grant(&uri.peer.to_hex(), &request.method, kind)
};
if granted {
approved_response(&keys, &request)
diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs
index 1248714..cc85cb8 100644
--- a/src/signer/nip46_client.rs
+++ b/src/signer/nip46_client.rs
@@ -1382,6 +1382,8 @@ impl Nip46ClientSigner {
} else {
None
},
+ permissions: connection.as_ref().and_then(|c| c.permissions.clone()),
+ expires_at: connection.as_ref().and_then(|c| c.expires_at),
}
}
@@ -1431,8 +1433,17 @@ impl Nip46ClientSigner {
(entry, peer)
};
if approved && always && !peer_hex.is_empty() {
+ // A "sign_event" always-allow covers only the kinds of the
+ // request the user actually saw — never other kinds. Other
+ // gated methods have no kind dimension.
+ let kinds: Vec = if entry.method == "sign_event" {
+ entry.details.event_kind.into_iter().collect()
+ } else {
+ Vec::new()
+ };
let mut app = self.app.lock().await;
- app.vault.grant_signer_method(&peer_hex, &entry.method)?;
+ app.vault
+ .grant_signer_method(&peer_hex, &entry.method, &kinds)?;
app.save_vault()?;
}
let _ = entry.sender.send(if approved {
@@ -1906,16 +1917,14 @@ impl Nip46ClientSigner {
}
// Check method permissions
+ let event_kind = request
+ .params
+ .first()
+ .and_then(|json| serde_json::from_str::(json).ok())
+ .and_then(|v| v.get("kind").and_then(|k| k.as_u64()))
+ .map(|k| k as u16);
let allowed = match request.method.as_str() {
- "sign_event" => {
- let kind = request
- .params
- .first()
- .and_then(|json| serde_json::from_str::(json).ok())
- .and_then(|v| v.get("kind").and_then(|k| k.as_u64()))
- .unwrap_or(0) as u16;
- self.can_sign_event(kind).await
- }
+ "sign_event" => self.can_sign_event(event_kind.unwrap_or(0)).await,
"nip44_encrypt" => self.can_encrypt().await,
"nip44_decrypt" => self.can_decrypt().await,
_ => false,
@@ -1929,9 +1938,10 @@ impl Nip46ClientSigner {
));
}
- // Standing grant ("always allow") for this peer + method: skip the
- // prompt and run. Grants are per (peer pubkey, method) and revocable
- // from the Signer screen.
+ // Standing grant ("always allow") for this peer + method + kind:
+ // skip the prompt and run. Grants are per (peer pubkey, method,
+ // kind-set) and revocable from the Signer screen — a kind-1 grant
+ // never covers a kind-3 request.
{
let peer_hex = self
.inner
@@ -1943,7 +1953,10 @@ impl Nip46ClientSigner {
.unwrap_or_default();
if !peer_hex.is_empty() {
let app = self.app.lock().await;
- if app.vault.has_signer_grant(&peer_hex, &request.method) {
+ if app
+ .vault
+ .has_signer_grant(&peer_hex, &request.method, event_kind)
+ {
drop(app);
self.inner.lock().await.phase = Nip46Phase::Connected;
return self.approved_response(keys, request);
diff --git a/src/signer/types.rs b/src/signer/types.rs
index 1334e85..876ce83 100644
--- a/src/signer/types.rs
+++ b/src/signer/types.rs
@@ -98,6 +98,16 @@ pub struct Nip46Status {
/// `None` once paired or when not pairing.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pairing_uri: Option,
+ /// The declared per-connection permissions for the live session, when
+ /// the connect URI carried a `perms=` grant list. `None` means no local
+ /// grant list — enforcement is the signer app's own approval prompts.
+ /// Surfaced so the UI can show what the connection was granted.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub permissions: Option,
+ /// When the live connection expires (unix timestamp), if it has a
+ /// deadline. Expired connections are refused at request time.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub expires_at: Option,
}
/// A pending approval request from the signer.
diff --git a/src/vault.rs b/src/vault.rs
index 0e4d907..0791f26 100644
--- a/src/vault.rs
+++ b/src/vault.rs
@@ -149,6 +149,16 @@ pub struct SignerGrant {
/// The gated NIP-46 method covered: `sign_event`, `nip44_encrypt`,
/// or `nip44_decrypt`.
pub method: String,
+ /// Event kinds covered when `method` is `sign_event`.
+ ///
+ /// A grant is created "always allow" against ONE concrete request, so a
+ /// new `sign_event` grant carries exactly the kinds of that request.
+ /// A non-empty list restricts the grant to those kinds; an empty list
+ /// means all kinds — possible only on legacy grants (written before
+ /// grants were kind-scoped), never created anew. Grants for other
+ /// methods always leave this empty.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub allowed_kinds: Vec,
/// Unix timestamp of when the user granted it.
pub created_at: u64,
}
@@ -206,22 +216,44 @@ impl Vault {
}
/// Whether `app_pubkey` may run gated `method` without a prompt.
- pub fn has_signer_grant(&self, app_pubkey: &str, method: &str) -> bool {
- self.signer_grants
- .iter()
- .any(|g| g.app_pubkey == app_pubkey && g.method == method)
+ ///
+ /// For `sign_event` the grant is kind-scoped: a grant recorded for
+ /// specific kinds does NOT cover other kinds — an uncovered kind falls
+ /// back to the approval prompt. `None` for `event_kind` means the
+ /// request has no kind dimension (encrypt/decrypt) or the kind could
+ /// not be parsed; a kind-restricted grant never answers `None`, so
+ /// unparseable kinds fail closed toward prompting.
+ pub fn has_signer_grant(
+ &self,
+ app_pubkey: &str,
+ method: &str,
+ event_kind: Option,
+ ) -> bool {
+ self.signer_grants.iter().any(|g| {
+ g.app_pubkey == app_pubkey
+ && g.method == method
+ && (g.allowed_kinds.is_empty()
+ || event_kind.is_some_and(|k| g.allowed_kinds.contains(&k)))
+ })
}
/// Record an "always allow" grant (idempotent).
+ ///
+ /// `allowed_kinds` scopes a `sign_event` grant to the kinds of the
+ /// request the user actually approved. Pass an empty slice for
+ /// non-signing methods and for kinds the user did not see — a new
+ /// grant never silently covers more than the request behind it.
pub fn grant_signer_method(
&mut self,
app_pubkey: &str,
method: &str,
+ allowed_kinds: &[u16],
) -> Result<(), crate::errors::AppError> {
- if !self.has_signer_grant(app_pubkey, method) {
+ if !self.has_signer_grant(app_pubkey, method, allowed_kinds.first().copied()) {
self.signer_grants.push(SignerGrant {
app_pubkey: app_pubkey.to_string(),
method: method.to_string(),
+ allowed_kinds: allowed_kinds.to_vec(),
created_at: crate::vault::unix_timestamp()?,
});
}
@@ -796,24 +828,53 @@ mod tests {
#[test]
fn signer_grants_roundtrip_and_revoke() {
let mut vault = Vault::empty();
- assert!(!vault.has_signer_grant("aa", "sign_event"));
+ assert!(!vault.has_signer_grant("aa", "sign_event", Some(1)));
- vault.grant_signer_method("aa", "sign_event").unwrap();
- vault.grant_signer_method("aa", "sign_event").unwrap(); // idempotent
- vault.grant_signer_method("bb", "sign_event").unwrap();
+ vault.grant_signer_method("aa", "sign_event", &[1]).unwrap();
+ vault.grant_signer_method("aa", "sign_event", &[1]).unwrap(); // idempotent
+ vault.grant_signer_method("bb", "sign_event", &[1]).unwrap();
assert_eq!(vault.signer_grants.len(), 2);
- assert!(vault.has_signer_grant("aa", "sign_event"));
- assert!(!vault.has_signer_grant("aa", "nip04_decrypt"));
+ assert!(vault.has_signer_grant("aa", "sign_event", Some(1)));
+ assert!(!vault.has_signer_grant("aa", "nip04_decrypt", None));
let json = serde_json::to_string(&vault).unwrap();
let mut loaded: Vault = serde_json::from_str(&json).unwrap();
- assert!(loaded.has_signer_grant("aa", "sign_event"));
- assert!(loaded.has_signer_grant("bb", "sign_event"));
+ assert!(loaded.has_signer_grant("aa", "sign_event", Some(1)));
+ assert!(loaded.has_signer_grant("bb", "sign_event", Some(1)));
assert!(loaded.revoke_signer_grant("aa", "sign_event"));
assert!(!loaded.revoke_signer_grant("aa", "sign_event"));
- assert!(!loaded.has_signer_grant("aa", "sign_event"));
- assert!(loaded.has_signer_grant("bb", "sign_event"));
+ assert!(!loaded.has_signer_grant("aa", "sign_event", Some(1)));
+ assert!(loaded.has_signer_grant("bb", "sign_event", Some(1)));
+ }
+
+ #[test]
+ fn signer_grants_are_kind_scoped() {
+ let mut vault = Vault::empty();
+ // A grant made against a kind-1 request must not cover kind-3.
+ vault.grant_signer_method("aa", "sign_event", &[1]).unwrap();
+ assert!(vault.has_signer_grant("aa", "sign_event", Some(1)));
+ assert!(!vault.has_signer_grant("aa", "sign_event", Some(3)));
+ // An unparseable kind (None) also falls back to the prompt.
+ assert!(!vault.has_signer_grant("aa", "sign_event", None));
+
+ // A legacy grant with no kind list (written before grants were
+ // kind-scoped) still covers every kind — stored vaults keep working.
+ vault.signer_grants.push(SignerGrant {
+ app_pubkey: "legacy".to_string(),
+ method: "sign_event".to_string(),
+ allowed_kinds: Vec::new(),
+ created_at: 0,
+ });
+ assert!(vault.has_signer_grant("legacy", "sign_event", Some(1)));
+ assert!(vault.has_signer_grant("legacy", "sign_event", Some(30023)));
+ assert!(vault.has_signer_grant("legacy", "sign_event", None));
+
+ // Non-signing methods carry no kind dimension.
+ vault
+ .grant_signer_method("aa", "nip44_decrypt", &[])
+ .unwrap();
+ assert!(vault.has_signer_grant("aa", "nip44_decrypt", None));
}
static COUNTER: AtomicU32 = AtomicU32::new(0);
From fa59b63a1725dc6c629e76eaa8ac4faf127d5d13 Mon Sep 17 00:00:00 2001
From: Avi
Date: Sun, 27 Sep 2026 22:43:42 -0500
Subject: [PATCH 06/17] fix(updates): augment spawned PATH so npm/cargo resolve
from Electron
The backend spawned by Electron inherits the desktop launcher's
environment, not a login shell: ~/.cargo/bin and the mise/asdf shim
dirs are missing, so 'Check for updates' failed with "'npm' was not
found" even though both tools exist in a terminal.
run() now sets PATH to the inherited value plus the well-known per-user
tool dirs (~/.cargo/bin, ~/.local/bin, mise + asdf shims,
/usr/local/bin), appended after the inherited entries so existing
resolutions keep priority. The NotFound hint now says what to do.
Verified live: update_check under env -i PATH=/usr/bin:/bin returns a
full report (npm advisories + cargo updates) where it previously errored.
Unit tests cover the pure PATH builder incl. missing-env fallback.
---
src/updates.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 79 insertions(+), 2 deletions(-)
diff --git a/src/updates.rs b/src/updates.rs
index ef75d9b..2217a15 100644
--- a/src/updates.rs
+++ b/src/updates.rs
@@ -72,6 +72,51 @@ fn frontend_dir() -> PathBuf {
source_dir().join("frontend")
}
+/// PATH used for package-manager commands.
+///
+/// When the backend is spawned by Electron its environment is the desktop
+/// launcher's, NOT a login shell: `~/.cargo/bin` (cargo) and the node
+/// version-manager bins/shims (npm) are missing, so the update commands
+/// failed with "'npm' was not found" even though both exist in a terminal.
+/// Augment the inherited PATH with the well-known per-user tool locations,
+/// appended after the inherited entries: where a tool is already resolvable
+/// on the inherited PATH that version wins, and entries that do not exist
+/// are simply ignored by exec.
+fn augmented_path() -> std::ffi::OsString {
+ let inherited = std::env::var_os("PATH");
+ let home = std::env::var_os("HOME");
+ build_augmented_path(inherited.as_deref(), home.as_deref())
+}
+
+/// Pure form of [`augmented_path`], testable without mutating the process
+/// environment.
+fn build_augmented_path(
+ inherited: Option<&std::ffi::OsStr>,
+ home: Option<&std::ffi::OsStr>,
+) -> std::ffi::OsString {
+ let inherited_parts: Vec = inherited
+ .map(std::env::split_paths)
+ .map(|p| p.collect())
+ .unwrap_or_default();
+ let home = home.map(PathBuf::from);
+ let extra: Vec = [
+ ".cargo/bin",
+ ".local/bin",
+ ".local/share/mise/shims",
+ ".asdf/shims",
+ "/usr/local/bin",
+ ]
+ .into_iter()
+ .filter_map(|rel| match rel.strip_prefix('/') {
+ // A leading slash marks an absolute system entry: use it verbatim.
+ Some(_) => Some(PathBuf::from(rel)),
+ None => home.as_ref().map(|h| h.join(rel)),
+ })
+ .collect();
+ std::env::join_paths(inherited_parts.into_iter().chain(extra))
+ .unwrap_or_else(|_| std::ffi::OsString::from("/usr/local/bin:/usr/bin:/bin"))
+}
+
/// Verify the app is running from its source checkout before shelling out.
fn require_source_checkout() -> Result<(), AppError> {
let manifest = source_dir().join("Cargo.toml");
@@ -88,13 +133,22 @@ fn require_source_checkout() -> Result<(), AppError> {
/// Run a command with a timeout, capturing stdout/stderr separately.
async fn run(dir: &Path, program: &str, args: &[&str]) -> Result {
let mut command = Command::new(program);
- command.current_dir(dir).args(args).kill_on_drop(true);
+ command
+ .current_dir(dir)
+ .args(args)
+ // See augmented_path(): Electron-spawned backends inherit a desktop
+ // PATH without the per-user tool dirs, and npm/cargo "don't exist".
+ .env("PATH", augmented_path())
+ .kill_on_drop(true);
let future = command.output();
match tokio::time::timeout(COMMAND_TIMEOUT, future).await {
Ok(Ok(output)) => Ok(output),
Ok(Err(err)) => {
let hint = if err.kind() == std::io::ErrorKind::NotFound {
- format!("'{program}' was not found on this computer.")
+ format!(
+ "'{program}' was not found on this computer. \
+ Install it (or add it to the app's PATH) and try again."
+ )
} else {
format!("Could not run '{program}': {err}")
};
@@ -355,6 +409,29 @@ pub async fn apply() -> Result {
mod tests {
use super::*;
+ #[test]
+ fn augmented_path_appends_tool_dirs_after_inherited() {
+ let joined = build_augmented_path(
+ Some(std::ffi::OsStr::new("/usr/bin:/bin")),
+ Some(std::ffi::OsStr::new("/home/tester")),
+ );
+ let joined = joined.to_string_lossy().into_owned();
+ // Inherited entries keep priority.
+ assert!(joined.starts_with("/usr/bin:/bin:"));
+ // The dirs an Electron-spawned process is missing are now present.
+ assert!(joined.contains("/home/tester/.cargo/bin"));
+ assert!(joined.contains("/home/tester/.local/share/mise/shims"));
+ assert!(joined.contains("/usr/local/bin"));
+ }
+
+ #[test]
+ fn augmented_path_survives_missing_env() {
+ // No PATH and no HOME: still a usable absolute system path.
+ let joined = build_augmented_path(None, None);
+ let joined = joined.to_string_lossy().into_owned();
+ assert!(joined.contains("/usr/local/bin"));
+ }
+
#[test]
fn parses_npm_outdated_entries() {
let json = r#"{
From 118f5d37bd8609837c4d7ae5d7dc1da4778adc48 Mon Sep 17 00:00:00 2001
From: Avi
Date: Sun, 27 Sep 2026 22:44:52 -0500
Subject: [PATCH 07/17] docs(checkpoint): permissions UI + updater PATH fix at
fa59b63 (2026-09-27)
---
CHECKPOINT-encryption.md | 64 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index 9f3d5d9..97f173b 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -1,3 +1,67 @@
+# Checkpoint — permissions UI + updater PATH fix (2026-09-27 night)
+
+## Where things are
+- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`fa59b63`**
+ ("fix(updates): augment spawned PATH so npm/cargo resolve from Electron").
+ Previous: `adbc7c2` (permissions UI), `98593cb` (checkpoint), `c89b31a`.
+- Working tree: clean for tracked files. Untracked intentionally NOT
+ committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
+ `deferred/`.
+- Release binary rebuilt at fa59b63 (22:43, verified mtime — not a cache hit).
+- NOTE: a running Electron app still serves the OLD backend + stale `dist/`
+ until relaunch; the running serve predates both commits.
+
+## What was completed
+1. **Permissions are visible (Step 4, first slice).** `Nip46Status` now
+ carries the connection's declared `perms=` grant list and expiry. The
+ Signer Mode screen shows a **Permissions** panel on a live session: one
+ row per granted method ("Sign events — kinds 1, 30023"), or a plain
+ statement that the signer app (Amber) approves every request when no
+ grant list was declared. `frontend/src/lib/permissions.ts` holds the
+ shared label formatters.
+2. **"Always allow" is now kind-scoped (was: method-wide, too broad).**
+ A `sign_event` grant records the kind of the request the user actually
+ approved; a kind-1 grant never covers a kind-3 request — uncovered kinds
+ fall back to the approval prompt. Legacy kind-less grants keep their
+ all-kinds meaning (stored vaults keep working; new grants are never
+ created kind-less). Enforced in BOTH `bunker.rs` (bunker mode) and
+ `nip46_client.rs` (client mode) via `Vault::has_signer_grant(peer,
+ method, event_kind)`. The Signer screen's grants list renders the human
+ label with kind scope.
+3. **Check-for-updates fixed.** The Electron-spawned backend inherited the
+ desktop launcher's PATH (no `~/.cargo/bin`, no mise/asdf shims), so
+ `npm`/`cargo` "didn't exist". `updates.rs::run()` now appends the
+ well-known per-user tool dirs to the inherited PATH (inherited wins on
+ conflicts; missing dirs ignored). Verified live under
+ `env -i PATH=/usr/bin:/bin`: `update_check` returns a full report.
+
+## Commits added
+- `adbc7c2` feat(signer): permissions UI — declared grants surfaced, always-allow kind-scoped
+- `fa59b63` fix(updates): augment spawned PATH so npm/cargo resolve from Electron
+
+## Verification (all green at fa59b63)
+- Rust: `cargo test` 219 unit + 6 e2e (NEW: `signer_grants_are_kind_scoped`,
+ two `augmented_path` tests); `cargo clippy --all-targets` 0; `cargo fmt
+ --check` clean; `cargo build --release` rebuilt 22:43.
+- Frontend: `npm test` 135 (10 new: `permissions.test.ts` unit + 2
+ SignerModeScreen permission-panel tests), `typecheck`, `lint`,
+ `format:check`, `build`, `electron:build` all green.
+
+## Deferred / next steps
+- Live eyeball: relaunch the app, pair with a `perms=`-carrying client (or
+ Amber) and check the Permissions panel; approve kind-1 "Always allow",
+ then send a kind-3 request and confirm it PROMPTS (kind scope).
+- Two-account live pass from the previous checkpoint still open (pair
+ account B in Amber, switch back and forth).
+- Publish kind-0 to primal/damus (one Amber approval).
+- Step 4 remainder (if wanted): interactive grant editing in the approval
+ modal (approve-with-narrowing UI); today the modal is Approve / Always
+ allow (kind-scoped) / Reject.
+- Step 5 (KDF upgrade m=64MiB/t=3 + vault header versioning), Step 6 (undo
+ history), Step 7 (rename/hygiene incl. `homepage` URL).
+
+---
+
# Checkpoint — multi-account signer switching (Option A) (2026-09-27 eve)
## Where things are
From 15fa331f46b848a8993258b9bb8899cbdbdbf11a Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 08:30:48 -0500
Subject: [PATCH 08/17] chore(deps): apply dependency updates from the in-app
updater run
Applying npm audit fix + npm update + cargo update via the (fixed) updater: clears the high-severity js-yaml advisory (maxTotalMergeKeys CPU use on empty merge sources). Full suites verified green after the bump: 219 Rust unit + 6 e2e, 135 frontend, vite build clean.
---
Cargo.lock | 297 ++++++++-------
frontend/package-lock.json | 721 ++++++++++++++++++++-----------------
2 files changed, 556 insertions(+), 462 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 0c2df61..47804f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -214,7 +214,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -292,12 +292,12 @@ dependencies = [
[[package]]
name = "bitcoin-consensus-encoding"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6712f9c6fd6785b3b270884e57c441c403dc5d7e19ca45368c97c7a1de3000ec"
+checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f"
dependencies = [
- "bitcoin-internals",
- "hex-conservative 1.2.0",
+ "bitcoin-internals 0.7.0",
+ "hex-conservative 1.3.0",
"serde",
]
@@ -307,6 +307,12 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5"
+[[package]]
+name = "bitcoin-internals"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2"
+
[[package]]
name = "bitcoin-io"
version = "0.1.101"
@@ -323,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2"
dependencies = [
"bitcoin-io",
- "hex-conservative 0.2.2",
+ "hex-conservative 0.2.3",
]
[[package]]
@@ -333,16 +339,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e"
dependencies = [
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "hex-conservative 1.2.0",
+ "bitcoin-internals 0.6.0",
+ "hex-conservative 1.3.0",
"serde",
]
[[package]]
name = "bitflags"
-version = "2.13.1"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "blake2"
@@ -440,9 +452,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.4.4"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
+checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -450,9 +462,9 @@ dependencies = [
[[package]]
name = "cfg-if"
-version = "1.0.4"
+version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chacha20"
@@ -562,9 +574,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.22"
+version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crypto-common"
@@ -610,6 +622,46 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+[[package]]
+name = "defmt"
+version = "0.3.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
+dependencies = [
+ "defmt 1.1.1",
+]
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.21",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -641,7 +693,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -715,10 +767,12 @@ dependencies = [
[[package]]
name = "faster-hex"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
+checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed"
dependencies = [
+ "autocfg",
+ "defmt 0.3.100",
"heapless",
"serde",
]
@@ -731,9 +785,9 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
-version = "0.1.11"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "form_urlencoded"
@@ -813,7 +867,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -951,18 +1005,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-conservative"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08"
dependencies = [
"arrayvec",
]
[[package]]
name = "hex-conservative"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10"
+checksum = "271e0d19bcb473b6675739a2b536076b24a082316cb5199ad918edce10c599e8"
dependencies = [
"arrayvec",
]
@@ -1003,9 +1057,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hybrid-array"
-version = "0.4.14"
+version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
+checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"typenum",
]
@@ -1116,9 +1170,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.14.1"
+version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
@@ -1152,9 +1206,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
-version = "0.3.104"
+version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
@@ -1241,9 +1295,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
-version = "0.18.2"
+version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a"
+checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
[[package]]
name = "memchr"
@@ -1262,9 +1316,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.2.2"
+version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"wasi",
@@ -1279,9 +1333,9 @@ checksum = "81c353b400a5503efdcf398f11a83fb7aa84f59f5d76fc4bf5bbc1e4f5366caa"
[[package]]
name = "nostr"
-version = "0.45.3"
+version = "0.45.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0ba32ce43631188586469ba1a4c40bcfa63641f1ad5de89ef77f74d801cc17a"
+checksum = "38ca5c25a6df8d4d78fdf39b42792438f6ce22b4809ccbfdb27582ed9ca45407"
dependencies = [
"aes 0.8.4",
"base64",
@@ -1293,7 +1347,7 @@ dependencies = [
"chacha20poly1305",
"faster-hex",
"opaquerr",
- "rand 0.10.2",
+ "rand 0.10.3",
"secp256k1",
"serde",
"serde_json",
@@ -1305,9 +1359,9 @@ dependencies = [
[[package]]
name = "nostr-database"
-version = "0.45.1"
+version = "0.45.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b1fdb9fcba732e32719662afad1b267e50322dbe89e506017ec13f24361bddf"
+checksum = "309950383c9854ca413d195705400e78b1376aedc48f3256754a86c609c047e8"
dependencies = [
"nostr",
"opaquerr",
@@ -1315,9 +1369,9 @@ dependencies = [
[[package]]
name = "nostr-gossip"
-version = "0.45.0"
+version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa07539e52a71cb91fe0d693facaa298f03fcf9edcd66a521094e18e286e2336"
+checksum = "4ea822fd48ff6b84b81ddf84169e6edf1dc0bc9b312c8e41685b2278debaac3d"
dependencies = [
"nostr",
"opaquerr",
@@ -1325,9 +1379,9 @@ dependencies = [
[[package]]
name = "nostr-sdk"
-version = "0.45.2"
+version = "0.45.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "245f8642b10feecb0a40739a886ca1850e3be4e35dc6592b806ec437403ab9c9"
+checksum = "db717044f755b5ed9be53cacb59660c36efbe578bde870151a24d6bead3cb218"
dependencies = [
"async-utility",
"async-wsocket",
@@ -1339,7 +1393,7 @@ dependencies = [
"nostr-database",
"nostr-gossip",
"opaquerr",
- "rand 0.10.2",
+ "rand 0.10.3",
"tokio",
"tokio-stream",
"tracing",
@@ -1626,9 +1680,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.10.2"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
dependencies = [
"getrandom 0.4.3",
"rand_core 0.10.1",
@@ -1684,7 +1738,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
]
[[package]]
@@ -1743,21 +1797,21 @@ dependencies = [
[[package]]
name = "rtoolbox"
-version = "0.0.5"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
+checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "rustix"
-version = "1.1.4"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys",
@@ -1766,9 +1820,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.43"
+version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"once_cell",
"ring",
@@ -1855,7 +1909,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -1899,7 +1953,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -1923,7 +1977,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -1983,9 +2037,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
-version = "1.15.2"
+version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
[[package]]
name = "socket2"
@@ -2022,9 +2076,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "3.0.4"
+version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -2033,13 +2087,13 @@ dependencies = [
[[package]]
name = "synstructure"
-version = "0.13.2"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.6",
]
[[package]]
@@ -2066,11 +2120,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.20"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
- "thiserror-impl 2.0.20",
+ "thiserror-impl 2.0.21",
]
[[package]]
@@ -2086,13 +2140,13 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.20"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -2107,18 +2161,9 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.12.0"
+version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
@@ -2154,14 +2199,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
name = "tokio-rustls"
-version = "0.26.4"
+version = "0.26.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
dependencies = [
"rustls",
"tokio",
@@ -2231,9 +2276,9 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.25.13+spec-1.1.0"
+version = "0.25.15+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
dependencies = [
"indexmap",
"toml_datetime",
@@ -2296,7 +2341,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"sha1",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"utf-8",
]
@@ -2319,9 +2364,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.24"
+version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-normalization"
@@ -2381,9 +2426,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.25.0"
+version = "1.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -2414,9 +2459,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.127"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
@@ -2427,19 +2472,20 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.77"
+version = "0.4.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
dependencies = [
"js-sys",
+ "tokio",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.127"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2447,31 +2493,31 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.127"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.6",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.127"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
-version = "0.3.104"
+version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2523,15 +2569,6 @@ dependencies = [
"windows-targets",
]
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -2639,13 +2676,13 @@ dependencies = [
[[package]]
name = "yoke-derive"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.6",
"synstructure",
]
@@ -2704,7 +2741,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -2732,18 +2769,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.56"
+version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
+checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.56"
+version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
+checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
dependencies = [
"proc-macro2",
"quote",
@@ -2761,13 +2798,13 @@ dependencies = [
[[package]]
name = "zerofrom-derive"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.6",
"synstructure",
]
@@ -2807,7 +2844,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
]
[[package]]
@@ -2840,7 +2877,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 3.0.4",
+ "syn 3.0.6",
"zvariant_utils",
]
@@ -2853,6 +2890,6 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
- "syn 3.0.4",
+ "syn 3.0.6",
"winnow",
]
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index d59b4a1..1d88fdc 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -250,9 +250,9 @@
"license": "MIT"
},
"node_modules/@electron/asar/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -439,9 +439,9 @@
"license": "MIT"
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
- "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
+ "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -449,9 +449,9 @@
}
},
"node_modules/@electron/universal/node_modules/fs-extra": {
- "version": "11.4.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
- "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz",
+ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -502,9 +502,9 @@
}
},
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
- "version": "11.4.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
- "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz",
+ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -583,9 +583,9 @@
"license": "MIT"
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -633,9 +633,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
- "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz",
+ "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -645,7 +645,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.3.0",
+ "js-yaml": "^4.3.2",
"minimatch": "^3.1.5",
"strip-json-comments": "^3.1.1"
},
@@ -656,6 +656,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/@eslint/eslintrc/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -664,9 +681,9 @@
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -674,6 +691,13 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -804,9 +828,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"dev": true,
"license": "MIT"
},
@@ -905,9 +929,9 @@
}
},
"node_modules/@noble/hashes": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz",
- "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -918,19 +942,19 @@
}
},
"node_modules/@oxc-project/types": {
- "version": "0.146.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz",
- "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==",
+ "version": "0.151.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz",
+ "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==",
"dev": true,
"license": "MIT",
"funding": {
- "url": "https://github.com/sponsors/Boshen"
+ "url": "https://github.com/sponsors/oxc-project"
}
},
"node_modules/@peculiar/asn1-schema": {
- "version": "2.9.4",
- "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz",
- "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.10.0.tgz",
+ "integrity": "sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -983,9 +1007,9 @@
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz",
- "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz",
+ "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==",
"cpu": [
"arm"
],
@@ -1000,9 +1024,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz",
- "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz",
+ "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==",
"cpu": [
"arm64"
],
@@ -1017,9 +1041,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz",
- "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz",
+ "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==",
"cpu": [
"arm64"
],
@@ -1034,9 +1058,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz",
- "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz",
+ "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==",
"cpu": [
"x64"
],
@@ -1051,9 +1075,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz",
- "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz",
+ "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==",
"cpu": [
"x64"
],
@@ -1068,9 +1092,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz",
- "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz",
+ "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==",
"cpu": [
"arm"
],
@@ -1085,13 +1109,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz",
- "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz",
+ "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1102,13 +1129,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz",
- "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz",
+ "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1119,13 +1149,16 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz",
- "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz",
+ "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==",
"cpu": [
"ppc64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1136,13 +1169,16 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz",
- "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz",
+ "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==",
"cpu": [
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1153,13 +1189,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz",
- "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz",
+ "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1170,13 +1209,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz",
- "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz",
+ "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1187,9 +1229,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz",
- "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz",
+ "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==",
"cpu": [
"arm64"
],
@@ -1204,9 +1246,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz",
- "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz",
+ "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==",
"cpu": [
"arm64"
],
@@ -1221,9 +1263,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz",
- "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz",
+ "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==",
"cpu": [
"x64"
],
@@ -1338,9 +1380,9 @@
}
},
"node_modules/@testing-library/dom": {
- "version": "10.4.1",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
- "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "version": "10.4.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1385,9 +1427,9 @@
"license": "MIT"
},
"node_modules/@testing-library/react": {
- "version": "16.3.2",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
- "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "version": "16.3.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1413,9 +1455,9 @@
}
},
"node_modules/@testing-library/user-event": {
- "version": "14.6.6",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz",
- "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==",
+ "version": "14.6.7",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz",
+ "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1523,13 +1565,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "26.2.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
- "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
+ "version": "26.6.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
+ "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~8.3.0"
+ "undici-types": "~8.9.0"
}
},
"node_modules/@types/prop-types": {
@@ -1581,17 +1623,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz",
- "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz",
+ "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/type-utils": "8.67.0",
- "@typescript-eslint/utils": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
+ "@typescript-eslint/scope-manager": "8.70.1",
+ "@typescript-eslint/type-utils": "8.70.1",
+ "@typescript-eslint/utils": "8.70.1",
+ "@typescript-eslint/visitor-keys": "8.70.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1604,15 +1646,15 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.67.0",
+ "@typescript-eslint/parser": "^8.70.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
- "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "version": "7.0.10",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz",
+ "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1620,16 +1662,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz",
- "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz",
+ "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
+ "@typescript-eslint/scope-manager": "8.70.1",
+ "@typescript-eslint/types": "8.70.1",
+ "@typescript-eslint/typescript-estree": "8.70.1",
+ "@typescript-eslint/visitor-keys": "8.70.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1645,14 +1687,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz",
- "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz",
+ "integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.67.0",
- "@typescript-eslint/types": "^8.67.0",
+ "@typescript-eslint/tsconfig-utils": "^8.70.1",
+ "@typescript-eslint/types": "^8.70.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1667,14 +1709,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
- "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz",
+ "integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0"
+ "@typescript-eslint/types": "8.70.1",
+ "@typescript-eslint/visitor-keys": "8.70.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1685,9 +1727,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz",
- "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz",
+ "integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1702,15 +1744,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz",
- "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz",
+ "integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0",
- "@typescript-eslint/utils": "8.67.0",
+ "@typescript-eslint/types": "8.70.1",
+ "@typescript-eslint/typescript-estree": "8.70.1",
+ "@typescript-eslint/utils": "8.70.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1727,9 +1769,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
- "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz",
+ "integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1741,16 +1783,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
- "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz",
+ "integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.67.0",
- "@typescript-eslint/tsconfig-utils": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
+ "@typescript-eslint/project-service": "8.70.1",
+ "@typescript-eslint/tsconfig-utils": "8.70.1",
+ "@typescript-eslint/types": "8.70.1",
+ "@typescript-eslint/visitor-keys": "8.70.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1769,16 +1811,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
- "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz",
+ "integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0"
+ "@typescript-eslint/scope-manager": "8.70.1",
+ "@typescript-eslint/types": "8.70.1",
+ "@typescript-eslint/typescript-estree": "8.70.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1793,13 +1835,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
- "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz",
+ "integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.67.0",
+ "@typescript-eslint/types": "8.70.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1824,9 +1866,9 @@
}
},
"node_modules/@vitejs/plugin-react": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz",
- "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
+ "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2020,16 +2062,16 @@
}
},
"node_modules/ajv": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
- "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@@ -2164,23 +2206,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/app-builder-lib/node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
"node_modules/app-builder-lib/node_modules/ci-info": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
@@ -2207,23 +2232,6 @@
"node": ">=6"
}
},
- "node_modules/app-builder-lib/node_modules/isexe": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
- "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/app-builder-lib/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/app-builder-lib/node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
@@ -2257,22 +2265,6 @@
"node": ">= 4.0.0"
}
},
- "node_modules/app-builder-lib/node_modules/which": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
- "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^3.1.1"
- },
- "bin": {
- "node-which": "bin/which.js"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2404,9 +2396,9 @@
"optional": true
},
"node_modules/brace-expansion": {
- "version": "5.0.9",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
- "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2719,6 +2711,29 @@
"node": ">= 8"
}
},
+ "node_modules/cross-spawn/node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -2949,9 +2964,9 @@
"license": "MIT"
},
"node_modules/dir-compare/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3063,9 +3078,9 @@
}
},
"node_modules/electron": {
- "version": "43.4.1",
- "resolved": "https://registry.npmjs.org/electron/-/electron-43.4.1.tgz",
- "integrity": "sha512-5b+EuiwkgG5iRcsEL34rimgRpkYp15SsfZOa0pC5kXs0Tb82TH4n95rpQzTZa7yRCbA7tm0WoEbuBL6NaAhAcA==",
+ "version": "43.7.5",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-43.7.5.tgz",
+ "integrity": "sha512-AR+OLH/8QAwx6Lz6mhaDQhesbjC/Xh2WOiFSI/CDFU6FKX/qJdBg65bEUn+MLvbvIi/LlaRQeLljnUcKb1KS0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3199,19 +3214,19 @@
}
},
"node_modules/electron/node_modules/@types/node": {
- "version": "24.13.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
- "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "version": "24.19.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz",
+ "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.18.0"
+ "undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/electron/node_modules/undici-types": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
@@ -3455,6 +3470,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/eslint/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3463,9 +3495,9 @@
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3473,6 +3505,13 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -3599,9 +3638,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz",
- "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==",
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"dev": true,
"funding": [
{
@@ -3664,9 +3703,9 @@
"license": "MIT"
},
"node_modules/filelist/node_modules/brace-expansion": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
- "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
+ "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3895,9 +3934,9 @@
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4294,11 +4333,14 @@
}
},
"node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
+ "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
"dev": true,
- "license": "ISC"
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/jake": {
"version": "10.9.4",
@@ -4335,9 +4377,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
- "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{
@@ -4406,9 +4448,9 @@
"license": "MIT"
},
"node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
@@ -4627,6 +4669,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4648,6 +4693,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4669,6 +4717,9 @@
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4690,6 +4741,9 @@
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4981,9 +5035,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.18",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
- "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "version": "3.3.19",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
"dev": true,
"funding": [
{
@@ -5007,9 +5061,9 @@
"license": "MIT"
},
"node_modules/node-abi": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz",
- "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==",
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.35.0.tgz",
+ "integrity": "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5075,9 +5129,9 @@
}
},
"node_modules/node-gyp/node_modules/undici": {
- "version": "6.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
- "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
+ "version": "6.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz",
+ "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5137,9 +5191,9 @@
}
},
"node_modules/nostr-tools": {
- "version": "2.25.1",
- "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.25.1.tgz",
- "integrity": "sha512-k/yCjpjHR18n9E6kCh1MdlP+fGZnP9UkuIDt1cHF87jqAE6ohOnZGFuQXPfWhDaRzNj9TQgJZPAPkCqOylqtAg==",
+ "version": "2.25.2",
+ "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.25.2.tgz",
+ "integrity": "sha512-7JAx+brEPmqGz1yzaK9MOxvis/Nl1B38j9hVsBSRDMRmxk5XSc0+5LlPEYbzeiUyk41RURBDcfuMzmaT754HAg==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "2.1.1",
@@ -5178,9 +5232,9 @@
"license": "MIT"
},
"node_modules/nwsapi": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
- "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "version": "2.2.28",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz",
+ "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==",
"dev": true,
"license": "MIT"
},
@@ -5196,9 +5250,9 @@
}
},
"node_modules/obug": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
- "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+ "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -5386,13 +5440,16 @@
}
},
"node_modules/pkijs": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
- "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz",
+ "integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==",
"dev": true,
"license": "BSD-3-Clause",
+ "workspaces": [
+ "website"
+ ],
"dependencies": {
- "@noble/hashes": "1.4.0",
+ "@noble/hashes": "1.8.0",
"asn1js": "^3.0.6",
"bytestreamjs": "^2.0.1",
"pvtsutils": "^1.3.6",
@@ -5404,13 +5461,13 @@
}
},
"node_modules/pkijs/node_modules/@noble/hashes": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
- "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 16"
+ "node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
@@ -5441,9 +5498,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.26",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
- "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
"dev": true,
"funding": [
{
@@ -5461,7 +5518,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.17",
+ "nanoid": "^3.3.18",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -5510,9 +5567,9 @@
}
},
"node_modules/prettier": {
- "version": "3.9.6",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
- "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+ "version": "3.9.9",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
+ "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -5988,13 +6045,13 @@
}
},
"node_modules/rolldown": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz",
- "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
+ "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.146.0",
+ "@oxc-project/types": "=0.151.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -6004,21 +6061,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm-eabi": "1.2.5",
- "@rolldown/binding-android-arm64": "1.2.5",
- "@rolldown/binding-darwin-arm64": "1.2.5",
- "@rolldown/binding-darwin-x64": "1.2.5",
- "@rolldown/binding-freebsd-x64": "1.2.5",
- "@rolldown/binding-linux-arm-gnueabihf": "1.2.5",
- "@rolldown/binding-linux-arm64-gnu": "1.2.5",
- "@rolldown/binding-linux-arm64-musl": "1.2.5",
- "@rolldown/binding-linux-ppc64-gnu": "1.2.5",
- "@rolldown/binding-linux-s390x-gnu": "1.2.5",
- "@rolldown/binding-linux-x64-gnu": "1.2.5",
- "@rolldown/binding-linux-x64-musl": "1.2.5",
- "@rolldown/binding-openharmony-arm64": "1.2.5",
- "@rolldown/binding-win32-arm64-msvc": "1.2.5",
- "@rolldown/binding-win32-x64-msvc": "1.2.5"
+ "@rolldown/binding-android-arm-eabi": "1.2.11",
+ "@rolldown/binding-android-arm64": "1.2.11",
+ "@rolldown/binding-darwin-arm64": "1.2.11",
+ "@rolldown/binding-darwin-x64": "1.2.11",
+ "@rolldown/binding-freebsd-x64": "1.2.11",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.11",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.11",
+ "@rolldown/binding-linux-arm64-musl": "1.2.11",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.11",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.11",
+ "@rolldown/binding-linux-x64-gnu": "1.2.11",
+ "@rolldown/binding-linux-x64-musl": "1.2.11",
+ "@rolldown/binding-openharmony-arm64": "1.2.11",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.11",
+ "@rolldown/binding-win32-x64-msvc": "1.2.11"
}
},
"node_modules/rrweb-cssom": {
@@ -6417,9 +6474,9 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
- "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6591,16 +6648,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz",
- "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==",
+ "version": "8.70.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.1.tgz",
+ "integrity": "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.67.0",
- "@typescript-eslint/parser": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0",
- "@typescript-eslint/utils": "8.67.0"
+ "@typescript-eslint/eslint-plugin": "8.70.1",
+ "@typescript-eslint/parser": "8.70.1",
+ "@typescript-eslint/typescript-estree": "8.70.1",
+ "@typescript-eslint/utils": "8.70.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6615,9 +6672,9 @@
}
},
"node_modules/undici": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
- "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "version": "7.30.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -6626,9 +6683,9 @@
}
},
"node_modules/undici-types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true,
"license": "MIT"
},
@@ -6696,16 +6753,16 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
- "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz",
+ "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
- "picomatch": "^4.0.5",
- "postcss": "^8.5.26",
- "rolldown": "~1.2.4",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.28",
+ "rolldown": "~1.2.9",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -6722,7 +6779,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.4.0 || ^0.5.0",
+ "@vitejs/devtools": "^0.7.1",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -6939,19 +6996,19 @@
}
},
"node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
"dev": true,
"license": "ISC",
"dependencies": {
- "isexe": "^2.0.0"
+ "isexe": "^3.1.1"
},
"bin": {
- "node-which": "bin/node-which"
+ "node-which": "bin/which.js"
},
"engines": {
- "node": ">= 8"
+ "node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/which-module": {
@@ -7013,9 +7070,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.21.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
- "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
"dev": true,
"license": "MIT",
"engines": {
From add956abdd4611ed6fa30b78e3951ac0652c90b8 Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 08:54:13 -0500
Subject: [PATCH 09/17] =?UTF-8?q?feat(theme):=20add=20Workshop=20theme=20?=
=?UTF-8?q?=E2=80=94=20Cybernetic=20Workshop=20identity=20from=20Moi=20DES?=
=?UTF-8?q?IGN.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Warm paper (#f3efe6), near-black ink, hairline borders, one pine accent (#215c48), serif display headings, Moi radii (14/9). Tokens verified rendering live via computed styles on the built shell: body bg #f3efe6, card #faf7f0, border #d9d1c3, h2 Iowan/Palatino 500. Settings -> Appearance -> 'Workshop — Cybernetic'.
---
frontend/src/lib/types.ts | 2 +-
frontend/src/screens/SettingsScreen.tsx | 1 +
frontend/src/styles.css | 64 +++++++++++++++++++++++++
src/settings.rs | 4 ++
4 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index a255dd9..ab62400 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -1,5 +1,5 @@
export type Theme =
- 'light' | 'dark' | 'glass' | 'neon' | 'impeccable' | 'impeccable-dark' | 'cosmic';
+ 'light' | 'dark' | 'glass' | 'neon' | 'impeccable' | 'impeccable-dark' | 'cosmic' | 'workshop';
/** Active signer mode. */
export type SignerMode = 'embedded' | 'nip46_bunker' | 'nip46_client';
diff --git a/frontend/src/screens/SettingsScreen.tsx b/frontend/src/screens/SettingsScreen.tsx
index 9be95c9..e7b6a2a 100644
--- a/frontend/src/screens/SettingsScreen.tsx
+++ b/frontend/src/screens/SettingsScreen.tsx
@@ -109,6 +109,7 @@ export function SettingsScreen() {
Impeccable — Light
Impeccable — Dark
Cosmic — Stardust
+ Workshop — Cybernetic
Surface -> Stone, shadow only where Moi allows it. */
+:root[data-theme='workshop'] {
+ --bg: #f3efe6;
+ --surface: #faf7f0;
+ --surface-2: #eae4d8;
+ --surface-hover: #faf7f0;
+ --border: #d9d1c3;
+ --border-strong: #c9bfad;
+ --text: #1c1814;
+ --text-muted: #5e574d;
+ --primary: #215c48;
+ --primary-hover: #184536;
+ --primary-soft: #e4f0ea;
+ --on-primary: #f3efe6;
+ --danger: #a13d2e;
+ --danger-soft: #f5e5e1;
+ --warning: #8b5a32;
+ --warning-soft: #f3ead9;
+ --success: #215c48;
+ --success-soft: #e4f0ea;
+ --info: #3c5a72;
+ --info-soft: #e6ecf1;
+ --focus: #215c48;
+ --shadow: 0 1px 2px rgba(28, 24, 20, 0.05), 0 8px 24px rgba(28, 24, 20, 0.06);
+ --shadow-modal: 0 12px 40px rgba(28, 24, 20, 0.18);
+ --radius: 14px;
+ --radius-sm: 9px;
+}
+
+/* Workshop type: Iowan/Palatino serif for display surfaces, everything
+ else stays on the local system stack (Moi ships no webfonts). */
+html[data-theme='workshop'] h1,
+html[data-theme='workshop'] h2 {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ font-weight: 500;
+ letter-spacing: -0.02em;
+}
+html[data-theme='workshop'] h1 {
+ font-size: 30px;
+ line-height: 1.05;
+}
+html[data-theme='workshop'] h2 {
+ font-size: 19px;
+ line-height: 1.2;
+}
+
+/* One accent rule: pine carries focus and active states; copper marks
+ index-like mono elements instead of a second saturated hue. */
+html[data-theme='workshop'] code,
+html[data-theme='workshop'] .mono {
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+}
+html[data-theme='workshop'] .sidebar {
+ background: var(--surface);
+ border-right-color: var(--border);
+}
+html[data-theme='workshop'] .card {
+ border-color: var(--border);
+}
+
* {
box-sizing: border-box;
}
diff --git a/src/settings.rs b/src/settings.rs
index d4eb9d3..11bcf40 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -28,6 +28,9 @@ pub enum Theme {
#[serde(rename = "impeccable-dark")]
ImpeccableDark,
Cosmic,
+ /// "Cybernetic Workshop" — the Moi portfolio look: warm paper, near-black
+ /// ink, hairline borders, one pine accent, serif headings.
+ Workshop,
}
impl Theme {
@@ -41,6 +44,7 @@ impl Theme {
"impeccable" => Some(Self::Impeccable),
"impeccable-dark" => Some(Self::ImpeccableDark),
"cosmic" => Some(Self::Cosmic),
+ "workshop" => Some(Self::Workshop),
_ => None,
}
}
From 17fd47c0812ca65519598f21640c33f892b64c3f Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 08:54:44 -0500
Subject: [PATCH 10/17] docs(checkpoint): Workshop theme + verified updater
apply at add956a (2026-09-28)
---
CHECKPOINT-encryption.md | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index 97f173b..9fbd101 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -1,15 +1,20 @@
-# Checkpoint — permissions UI + updater PATH fix (2026-09-27 night)
+# Checkpoint — permissions UI + updater fix + Workshop theme (2026-09-27 night)
## Where things are
-- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`fa59b63`**
- ("fix(updates): augment spawned PATH so npm/cargo resolve from Electron").
- Previous: `adbc7c2` (permissions UI), `98593cb` (checkpoint), `c89b31a`.
+- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`add956a`**
+ ("feat(theme): add Workshop theme — Cybernetic Workshop identity from Moi
+ DESIGN.md"). Previous: `15fa331` (updater-run dependency bumps, clears the
+ high js-yaml advisory), `fa59b63` (updater PATH fix), `adbc7c2`
+ (permissions UI), `98593cb` (checkpoint), `c89b31a`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/`.
-- Release binary rebuilt at fa59b63 (22:43, verified mtime — not a cache hit).
+- Release binary rebuilt at add956a (2026-09-28, after 3m01s real build).
- NOTE: a running Electron app still serves the OLD backend + stale `dist/`
- until relaunch; the running serve predates both commits.
+ until relaunch; the running serve predates these commits. The user's
+ "Update problem: The Rust backend exited unexpectedly (code 1)" screenshot
+ came from that old build; `update_apply` verified green end-to-end on the
+ new binary (all three steps applied).
## What was completed
1. **Permissions are visible (Step 4, first slice).** `Nip46Status` now
@@ -38,6 +43,8 @@
## Commits added
- `adbc7c2` feat(signer): permissions UI — declared grants surfaced, always-allow kind-scoped
- `fa59b63` fix(updates): augment spawned PATH so npm/cargo resolve from Electron
+- `15fa331` chore(deps): dependency updates from the in-app updater run (clears high js-yaml advisory)
+- `add956a` feat(theme): Workshop theme — Cybernetic Workshop identity from Moi DESIGN.md
## Verification (all green at fa59b63)
- Rust: `cargo test` 219 unit + 6 e2e (NEW: `signer_grants_are_kind_scoped`,
From c18f59ad6c61457f77b2f7407669cee52369f681 Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:02:17 -0500
Subject: [PATCH 11/17] =?UTF-8?q?feat(theme):=20Workshop=20=E2=80=94=20Dar?=
=?UTF-8?q?k,=20the=20Moi=20dark=20material?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moi DESIGN.md dark column: paper #12110f, surface #1c1a17, stone #26221c, warm ink #ebe6dc, pale pine #7eb89a. Same serif type and radii as the light Workshop theme. Verified rendering live via computed styles (body rgb(18,17,15), cards rgb(28,26,23), pine #7eb89a).
---
frontend/src/lib/types.ts | 10 ++++++-
frontend/src/screens/SettingsScreen.tsx | 1 +
frontend/src/styles.css | 38 +++++++++++++++++++++++--
src/settings.rs | 5 ++++
4 files changed, 51 insertions(+), 3 deletions(-)
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index ab62400..376ca84 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -1,5 +1,13 @@
export type Theme =
- 'light' | 'dark' | 'glass' | 'neon' | 'impeccable' | 'impeccable-dark' | 'cosmic' | 'workshop';
+ | 'light'
+ | 'dark'
+ | 'glass'
+ | 'neon'
+ | 'impeccable'
+ | 'impeccable-dark'
+ | 'cosmic'
+ | 'workshop'
+ | 'workshop-dark';
/** Active signer mode. */
export type SignerMode = 'embedded' | 'nip46_bunker' | 'nip46_client';
diff --git a/frontend/src/screens/SettingsScreen.tsx b/frontend/src/screens/SettingsScreen.tsx
index e7b6a2a..312bf32 100644
--- a/frontend/src/screens/SettingsScreen.tsx
+++ b/frontend/src/screens/SettingsScreen.tsx
@@ -110,6 +110,7 @@ export function SettingsScreen() {
Impeccable — Dark
Cosmic — Stardust
Workshop — Cybernetic
+ Workshop — Dark
Some(Self::ImpeccableDark),
"cosmic" => Some(Self::Cosmic),
"workshop" => Some(Self::Workshop),
+ "workshop-dark" => Some(Self::WorkshopDark),
_ => None,
}
}
From 8f1c5e0e18a1917608e587d7e31da3c931523daa Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:02:31 -0500
Subject: [PATCH 12/17] docs(checkpoint): Workshop Dark theme commit c18f59a
---
CHECKPOINT-encryption.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index 9fbd101..144c945 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -45,6 +45,7 @@
- `fa59b63` fix(updates): augment spawned PATH so npm/cargo resolve from Electron
- `15fa331` chore(deps): dependency updates from the in-app updater run (clears high js-yaml advisory)
- `add956a` feat(theme): Workshop theme — Cybernetic Workshop identity from Moi DESIGN.md
+- `c18f59a` feat(theme): Workshop — Dark, the Moi dark material
## Verification (all green at fa59b63)
- Rust: `cargo test` 219 unit + 6 e2e (NEW: `signer_grants_are_kind_scoped`,
From de804c8fc5aa9eb795004f7463bd72527555aaa4 Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:10:00 -0500
Subject: [PATCH 13/17] =?UTF-8?q?feat(theme):=20Workshop=20atmosphere=20?=
=?UTF-8?q?=E2=80=94=20Moi's=20graph-paper=20grid=20+=20mint/clay=20washes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Moi dark material is not just tokens: site.css paints a 24px hairline grid (rgba(235,230,220,0.035)) plus pine and clay radial washes over the paper. Applied the identical background stack to .main under both workshop themes (fixed attachment), with per-theme --wk-grid/--wk-wash-mint/--wk-wash-clay/--wk-copper tokens from Moi's light and dark blocks. Sidebar stays a clean opaque surface. Verified against the live Moi site in a browser: both render body #12110f with grid:true, wash:true, pine #7eb89a, copper #d4a574.
---
frontend/src/styles.css | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index b7f6dc1..f8f6bc6 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -600,6 +600,11 @@ html[data-theme='cosmic'] .empty-state {
--shadow-modal: 0 12px 40px rgba(28, 24, 20, 0.18);
--radius: 14px;
--radius-sm: 9px;
+ /* Moi atmosphere layer: hairline graph-paper grid + soft mint/clay washes */
+ --wk-grid: rgba(28, 24, 20, 0.04);
+ --wk-wash-mint: rgba(33, 92, 72, 0.1);
+ --wk-wash-clay: rgba(191, 112, 64, 0.1);
+ --wk-copper: #8b5a32;
}
/* Workshop type: Iowan/Palatino serif for display surfaces, everything
@@ -649,10 +654,37 @@ html[data-theme='workshop'] h2 {
--shadow-modal: 0 12px 40px rgba(0, 0, 0, 0.6);
--radius: 14px;
--radius-sm: 9px;
+ /* Moi dark atmosphere (site.css dark block): pale grid + pine/clay washes */
+ --wk-grid: rgba(235, 230, 220, 0.035);
+ --wk-wash-mint: rgba(126, 184, 154, 0.08);
+ --wk-wash-clay: rgba(212, 165, 116, 0.07);
+ --wk-copper: #d4a574;
}
/* One accent rule: pine carries focus and active states; copper marks
index-like mono elements instead of a second saturated hue. */
+html[data-theme='workshop'] .main,
+html[data-theme='workshop-dark'] .main {
+ /* Moi body canvas: graph-paper hairlines at 24px + mint/clay washes.
+ Applied to .main (the scroll surface) with fixed attachment so it
+ reads as the room, not a texture on a panel. */
+ background-image:
+ linear-gradient(var(--wk-grid) 1px, transparent 1px),
+ linear-gradient(90deg, var(--wk-grid) 1px, transparent 1px),
+ radial-gradient(50% 40% at 88% 8%, var(--wk-wash-mint), transparent 70%),
+ radial-gradient(45% 36% at 8% 92%, var(--wk-wash-clay), transparent 68%);
+ background-size:
+ 24px 24px,
+ 24px 24px,
+ auto,
+ auto;
+ background-attachment: fixed;
+}
+html[data-theme='workshop'] .sidebar,
+html[data-theme='workshop-dark'] .sidebar {
+ /* keep the sidebar a clean surface over the grid */
+ background: var(--surface);
+}
html[data-theme='workshop'] code,
html[data-theme='workshop-dark'] code,
html[data-theme='workshop'] .mono,
From ef7b79bfe44b1c4db55bd8f9587cd8f59124cbbf Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:10:18 -0500
Subject: [PATCH 14/17] docs(checkpoint): Workshop atmosphere commit de804c8
---
CHECKPOINT-encryption.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index 144c945..e6c07ee 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -46,6 +46,7 @@
- `15fa331` chore(deps): dependency updates from the in-app updater run (clears high js-yaml advisory)
- `add956a` feat(theme): Workshop theme — Cybernetic Workshop identity from Moi DESIGN.md
- `c18f59a` feat(theme): Workshop — Dark, the Moi dark material
+- `de804c8` feat(theme): Workshop atmosphere — Moi's graph-paper grid + mint/clay washes
## Verification (all green at fa59b63)
- Rust: `cargo test` 219 unit + 6 e2e (NEW: `signer_grants_are_kind_scoped`,
From 4cc04812fed0d2ec3517d32a27eac2a5da59508a Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:17:30 -0500
Subject: [PATCH 15/17] feat(theme): white logo mark on workshop-dark
The logo PNG is dark ink art; invert+brighten it on the dark workshop material (same technique cosmic uses) so it reads as warm paper ink on #12110f. Nav icons are currentColor and already themed. Verified filter present in built bundle.
---
frontend/src/styles.css | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index f8f6bc6..31e669f 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -426,6 +426,12 @@ html[data-theme='cosmic'] .sidebar-logo img {
filter: invert(1);
}
+/* The logo art is dark ink; on the dark workshop material it inverts to
+ the warm paper ink so the mark stays legible on #12110f. */
+html[data-theme='workshop-dark'] .sidebar-logo img {
+ filter: invert(1) grayscale(1) brightness(1.4);
+}
+
/* Cosmic workspace - Deep Space background with vignette effect */
html[data-theme='cosmic'] .app-shell {
background:
From dcc701f88b7e722d0e822dbb59ce9c1fe3bce871 Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:26:48 -0500
Subject: [PATCH 16/17] feat(updater): apply updates without restarting the app
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New app:selfupdate IPC (main process): npm run build + cargo build --release with the augmented PATH, then kill the backend child, reset the spawn flag so the next request starts the NEW binary, and reloadIgnoringCache every window. The Electron shell keeps running — no manual restart. Settings install now triggers it automatically when restart_required. Honest limits: a change to the Electron main process itself still needs one manual relaunch, and packaged builds report that bundle replacement is the update path.
---
frontend/electron/main.ts | 90 +++++++++++++++++++++++++
frontend/electron/preload.ts | 2 +
frontend/src/lib/api.ts | 2 +
frontend/src/screens/SettingsScreen.tsx | 20 +++++-
4 files changed, 113 insertions(+), 1 deletion(-)
diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts
index e6c79eb..9af5e94 100644
--- a/frontend/electron/main.ts
+++ b/frontend/electron/main.ts
@@ -394,6 +394,94 @@ function resolveWindowIcon(): string {
return path.join(base, 'icon.png');
}
+/**
+ * Well-known per-user tool dirs appended to the inherited PATH so npm/cargo
+ * resolve when Electron launches us from a desktop launcher (mirrors the
+ * backend's augmented_path() in src/updates.rs).
+ */
+function augmentedPath(): string {
+ const home = process.env.HOME ?? '';
+ const extra = [
+ `${home}/.cargo/bin`,
+ `${home}/.local/bin`,
+ `${home}/.mise/shims`,
+ `${home}/.asdf/shims`,
+ '/usr/local/bin',
+ ];
+ const inherited = process.env.PATH ?? '';
+ return [...inherited.split(':').filter(Boolean), ...extra].join(':');
+}
+
+/** Run a build command to completion, resolving with its combined output. */
+function runBuild(
+ cwd: string,
+ program: string,
+ args: string[],
+): Promise<{ ok: boolean; output: string }> {
+ return new Promise((resolve) => {
+ const child = spawn(program, args, {
+ cwd,
+ env: { ...process.env, PATH: augmentedPath() },
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ let output = '';
+ child.stdout?.on('data', (chunk: Buffer) => (output += chunk.toString()));
+ child.stderr?.on('data', (chunk: Buffer) => (output += chunk.toString()));
+ child.on('error', (err) => resolve({ ok: false, output: `${program}: ${err.message}` }));
+ child.on('close', (code) => resolve({ ok: code === 0, output }));
+ });
+}
+
+let selfUpdateInFlight: Promise<{ reloaded: boolean; note: string }> | null = null;
+
+/**
+ * Rebuild the app from its source checkout and hot-swap the running parts:
+ * fresh `dist/` + a freshly spawned backend, then reload every window. The
+ * Electron shell keeps running, so the user does not restart the app.
+ * (A change to this main-process file itself still needs a manual relaunch.)
+ */
+async function selfUpdate(): Promise<{ reloaded: boolean; note: string }> {
+ if (selfUpdateInFlight) return selfUpdateInFlight;
+ selfUpdateInFlight = (async () => {
+ if (app.isPackaged) {
+ throw new Error('This build is packaged; updates are applied by replacing the app bundle.');
+ }
+ const frontendDir = app.getAppPath();
+ const projectRoot = path.join(frontendDir, '..');
+
+ const npmBuild = await runBuild(frontendDir, 'npm', ['run', 'build']);
+ if (!npmBuild.ok) {
+ throw new Error(`Frontend build failed:\n${npmBuild.output.slice(-2000)}`);
+ }
+ const cargoBuild = await runBuild(projectRoot, 'cargo', ['build', '--release']);
+ if (!cargoBuild.ok) {
+ throw new Error(`Rust build failed:\n${cargoBuild.output.slice(-2000)}`);
+ }
+
+ // Swap the backend: kill the old child; the next request spawns the new
+ // binary. startBackend() re-checks exitCode, so resetting the flag is
+ // enough once the process is actually gone.
+ if (backend && backend.exitCode === null) {
+ backend.kill();
+ await new Promise((resolve) => {
+ if (!backend) return resolve();
+ backend.once('exit', () => resolve());
+ setTimeout(resolve, 3000);
+ });
+ }
+ backend = null;
+ backendStarted = false;
+
+ for (const window of BrowserWindow.getAllWindows()) {
+ window.webContents.reloadIgnoringCache();
+ }
+ return { reloaded: true, note: 'Rebuilt and reloaded. The app stayed open.' };
+ })().finally(() => {
+ selfUpdateInFlight = null;
+ });
+ return selfUpdateInFlight;
+}
+
function startBackend(): void {
if (backendStarted && backend && backend.exitCode === null) {
return;
@@ -985,6 +1073,8 @@ app.whenReady().then(() => {
},
);
+ ipcMain.handle('app:selfupdate', () => selfUpdate());
+
ipcMain.handle('clipboard:write', (_event, text: string) => {
clipboard.writeText(String(text));
return true;
diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts
index 93db526..3c7e5ac 100644
--- a/frontend/electron/preload.ts
+++ b/frontend/electron/preload.ts
@@ -4,4 +4,6 @@ contextBridge.exposeInMainWorld('backend', {
request: (method: string, params?: Record): Promise =>
ipcRenderer.invoke('backend:request', { method, params }),
copyText: (text: string): Promise => ipcRenderer.invoke('clipboard:write', text),
+ selfUpdate: (): Promise<{ reloaded: boolean; note: string }> =>
+ ipcRenderer.invoke('app:selfupdate'),
});
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index fc67375..3f1d0db 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -25,6 +25,8 @@ declare global {
backend: {
request(method: string, params?: Record): Promise;
copyText(text: string): Promise;
+ /** Rebuild + hot-reload the app without a manual restart (Electron only). */
+ selfUpdate?(): Promise<{ reloaded: boolean; note: string }>;
};
}
}
diff --git a/frontend/src/screens/SettingsScreen.tsx b/frontend/src/screens/SettingsScreen.tsx
index 312bf32..e2bc2bb 100644
--- a/frontend/src/screens/SettingsScreen.tsx
+++ b/frontend/src/screens/SettingsScreen.tsx
@@ -73,7 +73,25 @@ export function SettingsScreen() {
const result = await updateApply();
const lines = [...result.applied, ...result.failed.map((failure) => `Failed: ${failure}`)];
if (result.restart_required) {
- lines.push('Rebuild and restart the app (cargo build --release, then relaunch) to finish.');
+ // Rebuild in place and hot-swap the backend + renderer. The Electron
+ // shell itself never restarts; only a change to the main process file
+ // (this code's own host) still needs a manual relaunch.
+ if (window.backend.selfUpdate) {
+ lines.push('Rebuilding and reloading the app…');
+ setApplyMessage(lines);
+ try {
+ const update = await window.backend.selfUpdate();
+ lines.push(update.note);
+ } catch (err) {
+ lines.push(
+ `Automatic reload failed: ${err instanceof Error ? err.message : String(err)}. Rebuild and restart manually to finish.`,
+ );
+ }
+ } else {
+ lines.push(
+ 'Rebuild and restart the app (cargo build --release, then relaunch) to finish.',
+ );
+ }
}
setApplyMessage(lines.length > 0 ? lines : ['Everything is already up to date.']);
} catch (err) {
From 6bff1887145572b0752c9438aee9bd8363cdeebd Mon Sep 17 00:00:00 2001
From: Avi
Date: Mon, 28 Sep 2026 09:27:35 -0500
Subject: [PATCH 17/17] docs(checkpoint): white logo + no-restart self-update
commits
---
CHECKPOINT-encryption.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index e6c07ee..2271d5c 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -47,6 +47,8 @@
- `add956a` feat(theme): Workshop theme — Cybernetic Workshop identity from Moi DESIGN.md
- `c18f59a` feat(theme): Workshop — Dark, the Moi dark material
- `de804c8` feat(theme): Workshop atmosphere — Moi's graph-paper grid + mint/clay washes
+- `4cc0481` feat(theme): white logo mark on workshop-dark
+- `dcc701f` feat(updater): apply updates without restarting the app — `app:selfupdate` IPC rebuilds (npm + cargo, augmented PATH), kills the backend child so the next request spawns the NEW binary, reloads all windows. Electron shell stays up; main-process changes still need one manual relaunch; packaged builds report bundle replacement.
## Verification (all green at fa59b63)
- Rust: `cargo test` 219 unit + 6 e2e (NEW: `signer_grants_are_kind_scoped`,