From 07b227d1d8c35fe2b2397d965409f2d09dc5cd44 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 20:07:50 +0000 Subject: [PATCH 1/4] Send client metadata in NIP-46 bunker connect request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When pairing via a bunker:// URI, the initial NIP-46 connect handshake now carries jank's app metadata so the remote signer shows "Jank" / "jank.army" instead of a generic app name. This mirrors the name/url/image already advertised in the nostrconnect:// URI. nostr-tools' BunkerSigner.connect() only sends [pubkey, secret] and exposes no metadata arg, so we issue the connect request via sendRequest() ourselves with a 4th param. The metadata is a JSON *string* (not a nested object) since signers decode params as string[] and drop the whole array if any element is an object; the empty permissions slot is kept so metadata lands in slot 3. Reconnects (account switch) and the nostrconnect multi-account flow are unchanged — they intentionally skip the connect handshake. https://claude.ai/code/session_01BJDLuo6tBk2VwduekkPDnU --- .../__tests__/bunker.signer.spec.ts | 41 ++++++++++++++++++- src/providers/NostrProvider/bunker.signer.ts | 25 ++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/providers/NostrProvider/__tests__/bunker.signer.spec.ts b/src/providers/NostrProvider/__tests__/bunker.signer.spec.ts index 5bc4a9a..5242817 100644 --- a/src/providers/NostrProvider/__tests__/bunker.signer.spec.ts +++ b/src/providers/NostrProvider/__tests__/bunker.signer.spec.ts @@ -58,6 +58,9 @@ describe('BunkerSigner.login — reconnection does not block on the v3 probe', ( // The switch must not be blocked on the describe probe. expect(resolved).toBe(true) expect(value).toBe(POINTER_PK) + // Reconnection must not re-issue the connect handshake — metadata is only + // sent on the initial pairing; reconnects are unchanged. + expect(inner.sendRequest).not.toHaveBeenCalledWith('connect', expect.anything()) // Probe hasn't completed yet, so capability is still the safe default. expect(signer.supportsNip44v3()).toBe(false) @@ -69,7 +72,7 @@ describe('BunkerSigner.login — reconnection does not block on the v3 probe', ( await p }) - it('initial connection still awaits connect + the v3 probe before returning', async () => { + it('initial connection sends connect + awaits the v3 probe before returning', async () => { inner.sendRequest.mockImplementation((method: string) => method === 'describe' ? Promise.resolve(JSON.stringify(['nip44v3_encrypt', 'nip44v3_decrypt'])) @@ -79,10 +82,44 @@ describe('BunkerSigner.login — reconnection does not block on the v3 probe', ( const signer = new BunkerSigner(CLIENT_SK) const pubkey = await signer.login(BUNKER, true) - expect(inner.connect).toHaveBeenCalledTimes(1) + // connect is issued via sendRequest (not nostr-tools' connect() helper) so + // the metadata 4th param can be appended — the helper exposes no metadata arg. + expect(inner.sendRequest).toHaveBeenCalledWith('connect', expect.any(Array)) + expect(inner.connect).not.toHaveBeenCalled() expect(inner.getPublicKey).toHaveBeenCalledTimes(1) // Probe was awaited, so v3 is known by the time login resolves. expect(signer.supportsNip44v3()).toBe(true) expect(pubkey).toBe('11'.repeat(32)) }) + + it('appends jank client metadata as a JSON-string 4th param on the connect request', async () => { + inner.sendRequest.mockResolvedValue(null) + + const signer = new BunkerSigner(CLIENT_SK) + await signer.login(BUNKER, true) + + const connectCall = inner.sendRequest.mock.calls.find( + (call: unknown[]) => call[0] === 'connect' + ) + expect(connectCall).toBeDefined() + const params = connectCall![1] as unknown[] + + // [signer_pubkey, secret] come from the parsed bunker pointer. + expect(params[0]).toBe(POINTER_PK) + expect(params[1]).toBe('s') + // The permissions slot is kept (empty) so metadata still lands in slot 3. + expect(params[2]).toBe('') + expect(params).toHaveLength(4) + // CRITICAL: every element is a string. Signers decode `params` as string[] + // and discard the whole array if any element is a nested object. + expect(params.every((p) => typeof p === 'string')).toBe(true) + + // params[3] is a JSON *string*, not a nested object. + const meta = JSON.parse(params[3] as string) + // Keys are exactly name/url/image (use `image`, not imageURL/picture). + expect(Object.keys(meta).sort()).toEqual(['image', 'name', 'url']) + expect(meta.name).toBe('Jank') + expect(meta.url).toBe('https://jank.army') + expect(meta.image).toBe('https://jank.army/apple-touch-icon.png') + }) }) diff --git a/src/providers/NostrProvider/bunker.signer.ts b/src/providers/NostrProvider/bunker.signer.ts index 6c72ec6..fd4375e 100644 --- a/src/providers/NostrProvider/bunker.signer.ts +++ b/src/providers/NostrProvider/bunker.signer.ts @@ -1,3 +1,4 @@ +import { BRAND } from '@/branding' import { withSignerApproval } from '@/lib/signer-approval' import { ISigner, TDraftEvent } from '@/types' import { bytesToHex, hexToBytes } from '@noble/hashes/utils' @@ -34,7 +35,29 @@ export class BunkerSigner implements ISigner { } }) if (isInitialConnection) { - await this.signer.connect() + // Replicate nostr-tools' BunkerSigner.connect() but append a 4th param + // carrying jank's client metadata (name/url/image) so the remote signer + // (Clave/Amber/etc.) shows "Jank" instead of a generic app name. The + // library's connect() only sends [pubkey, secret] and exposes no metadata + // argument, so we issue the request ourselves. This mirrors the metadata + // jank already advertises in its nostrconnect:// URI. + // + // Wire contract: signers decode `params` as string[] and discard the whole + // array if any element is a non-string. params[2] is the permissions slot, + // kept as '' (jank requests none) so the metadata still lands in slot 3; + // params[3] is a JSON *string* (not a nested object), keys exactly + // name/url/image. + const clientMetadata = JSON.stringify({ + name: 'Jank', + url: BRAND.homepage, + image: `${BRAND.homepage}/apple-touch-icon.png` + }) + await this.signer.sendRequest('connect', [ + bunkerPointer.pubkey, + bunkerPointer.secret || '', + '', + clientMetadata + ]) // Initial connection (interactive pairing): block on the probe with a // timeout so the deck-sync flow has a definitive v3 answer before its // first encrypt. This path isn't latency-sensitive (one-time pairing). From 13086661f5f1a9861f92b01688cad5b2585a024e Mon Sep 17 00:00:00 2001 From: DocNR Date: Tue, 16 Jun 2026 23:28:37 -0400 Subject: [PATCH 2/4] feat(branding): terminal-styled share preview + JANK wordmark/name Match the share-link preview to the app's default terminal theme, and standardize the visible brand name as JANK. Preview (public/og-image.png via scripts/og-image/og-image.svg): near-black bg + phosphor-green creature + monospace JANK wordmark, mirroring the terminal-preset dark theme. og:image cache-buster bumped v2->v3 in index.html so scrapers re-fetch. JANK as the visible name: BRAND.name/shortName (tab title, og/twitter title, PWA name); sidebar wordmark in Logo.tsx (dropped the lowercase text-transform); NIP-46 bunker connect client name (+ test); current UI prose now interpolates the brand from BRAND.name instead of hardcoding (WhatsNew, UpdatePrompt, MobileWarningBanner, AgentPairingWizard, AgentsSettingsPage); MultiAccountConfirmModals + the TopBar aria-label are plain literals. Wire/machine identifiers stay lowercase on purpose: nostrClientTag, package.json name, deck-sync d-tag/alt, agent-pairing config key, MCP serverInfo.name. Co-Authored-By: Claude Opus 4.8 --- index.html | 4 +- public/og-image.png | Bin 7997 -> 9998 bytes scripts/og-image/og-image.svg | 47 +++++++++--------- src/assets/Logo.tsx | 6 +-- src/branding.ts | 4 +- .../MultiAccountConfirmModals/index.tsx | 2 +- src/components/AgentPairingWizard/index.tsx | 4 +- src/components/TopBar/MobileWarningBanner.tsx | 5 +- src/components/TopBar/index.tsx | 2 +- src/components/UpdatePrompt/index.tsx | 5 +- src/components/WhatsNew/index.tsx | 5 +- .../secondary/AgentsSettingsPage/index.tsx | 4 +- .../__tests__/bunker.signer.spec.ts | 2 +- src/providers/NostrProvider/bunker.signer.ts | 4 +- 14 files changed, 54 insertions(+), 40 deletions(-) diff --git a/index.html b/index.html index 56dfd65..359f8df 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,7 @@ - + @@ -38,7 +38,7 @@ - +