Commit Graph
908 Commits
Author SHA1 Message Date
Claude baf5c542a0 feat: favorite tabs use the app's own icon and drop the bottom-bar label
Two bottom-nav polish items:
- No label on favorite tabs — they now match the built-in items, which are
  icon-only.
- Use the app's own icon. FavoriteApp gains an optional iconUrl (the
  nsite/napplet manifest icon, captured when you favorite from its card and
  persisted). A shared FavoriteAppIcon renders that icon, falling back to a
  type glyph (the napplet/nsite mark, or the globe for a plain URL) when
  there's no icon or it fails to load. Used in both the bottom bar and the
  Favorite Apps grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:34:50 +00:00
Claude c21b592cb1 feat: favorite web apps — launcher, full-screen host, and bottom-bar grid
Introduces a device-local "favorite apps" system that unifies nsites,
napplets, and arbitrary web clients behind one model and three
presentation shells, while removing the editable address bar from any
running app (it now lives only in the browser launcher).

Core spine:
- FavoriteApp (commons): a sealed model with two cases — NostrApp
  (nsite/napplet, keyed by addressable coordinate so it survives code
  updates) and WebUrl. napplet-vs-nsite is recomputed from the live
  event at launch, never stored.
- FavoriteAppsRegistry (amethyst): device-local, DataStore-backed,
  StateFlow source of truth; main process only, hydrated at app start.
- FavoriteAppLauncher: dispatches a favorite to its one launch path —
  full-screen BrowserHostActivity for a URL, sandboxed NappletLauncher
  (re-resolved from LocalCache) for an nsite/napplet.

Presentation:
- BrowserHostActivity: full-screen, single-app host in the main process
  that embeds the keyless :napplet browser surface. Its own task/recents
  entry (documentLaunchMode=intoExisting), no editable URL — locked to
  the app it opened with, keeping one NIP-07 trust context per instance.
- BrowserScreen is now a launcher: an omnibox that opens each URL in its
  own host activity, plus the shared favorites grid. The address bar is
  gone from the content surface.
- FavoriteAppsScreen + FAVORITE_APPS bottom-bar item: a grid of big
  launch buttons, reused inside the browser launcher.
- StaticWebsiteCard gains a header-actions slot; a star toggle on each
  nsite/napplet card pins it (strings/store stay in the app layer).

EmbeddedBrowserSurface extracts the chrome-free surface + controller
helper so the tab, the launcher, and the host activity share one piece
of cross-process glue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 14:30:49 +00:00
Claude d63bf87d2a feat: add in-app browser tab rendered from the keyless napplet sandbox
Adds a "Browser" navigation destination (drawer + pinnable bottom-nav item,
API 30+) that opens any URL. The page renders in the sandboxed, keyless
`:napplet` process and is streamed into the main activity as a cross-process
surface via androidx.privacysandbox.ui (SurfaceControlViewHost) — only pixels
and input cross the boundary, never the WebView's JS context or the NIP-07
bridge. The trusted address bar is drawn by the main process around the
embedded surface, so the sandbox can never spoof the URL.

NIP-07 `window.nostr` is injected the same way nSite website mode does it, but
scoped per visited origin: each origin gets its own broker-minted launch token
(keyed by the trusted source origin), so a grant to one site never leaks to
another.

- NappletBrowserService (`:napplet`): hosts the live-URL WebView, exposes it as
  a SandboxedUiAdapter, and relays the per-origin NIP-07 bridge to the broker.
- NappletBrowserUiAdapter: wraps the WebView session for privacysandbox.ui.
- NappletBrokerService: mints a per-origin synthetic identity so NIP-07 consent
  is scoped per host.
- EmbeddedBrowserController + BrowserScreen: bind the service, render the
  SandboxedSdkView, and drive the trusted address bar (navigate/reload/back/Tor).
- shim.js: a direct-bridge transport so the injected shim works in a top-level
  page that has no trusted shell parent.
- Browser nav item hidden below API 30 (SurfaceControlViewHost requirement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 03:39:06 +00:00
Claude 2b80bf49e3 feat(napplet): broker the NAP inc topic bus
Adds the `inc` pub/sub bus so napplets that declare it boot and can exchange
topic events: `inc.subscribe {topic}` / `inc.unsubscribe {topic}` register
interest, `inc.emit {topic, payload}` fans out an `inc.event {topic, payload,
sender}` to OTHER subscribed napplet sessions (never echoing the sender) — the
kehto runtime's inc contract.

- Router edge ops (gated on the INC declaration alone, like identity.watch —
  no per-call consent): SubscribeInc/UnsubscribeInc/EmitInc outcomes.
- Protocol: readTopic/readPayloadRaw + encodeIncEvent.
- NappletIncBus in the broker service routes across the live napplet sessions
  (the one service every sandbox binds), keyed by reply Messenger.
- Tests for inc routing + declaration gating; updated capability/router tests
  that asserted the old "inc/theme/notify are unknown" behavior.

NOTE: napplets run foreground-only/one-at-a-time, so cross-napplet delivery is
usually a no-op in practice; the bus is correct if sessions ever overlap. It is
app-wide (not author-scoped) — a future refinement could namespace topics by
author. Unblocks feed/profile-viewer/chat/bot. See the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-23 00:37:44 +00:00
Claude 1599f34308 feat(napplet): broker the NAP notify domain
Napplets can now create/list/dismiss user-facing notifications:
`notify.create { title, body }` → `notify.created { id }` (the bespoke
past-tense reply the client listens for, not the generic `.result`),
`notify.list` → `notify.listed { notifications }`, `notify.dismiss` fire-and-forget.

- NotifyCreate/NotifyList/NotifyDismiss requests, NotifyCreated/NotifyListed
  responses, NappletNotifyGateway + NappletNotification; broker executes them
  (consent-gated, ask-once).
- Protocol encodes the past-tense reply types.
- Android: NappletNotificationStore (per-coordinate, main-process, survives
  broker rebuilds — a napplet only ever sees/dismisses its own) + a best-effort
  system-tray notification.
- Consent summary + string for notify.

Unblocks the toaster demo. See the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-23 00:30:42 +00:00
Claude 4b8b2ac513 feat(napplet): broker the NAP theme domain
Napplets can now read the host's current theme via `theme.get` →
`theme.get.result { theme: { colors: { background, text, primary } } }`,
mapping it to their CSS variables. This is the universal boot gate for
real-world napplets (e.g. kehto/web's demos all `requires: theme` and abort
if shell.supports('theme') is false).

- NappletCapability gains THEME (+ NOTIFY/INC, wired in following commits);
  adds requiresConsent (false for SHELL/THEME — cosmetic/negotiation never prompt).
- ThemeGet request, Theme response, NappletThemeGateway; broker executes it
  with no consent prompt.
- Android gateway returns Amethyst's brand purple with a dark/light bg+text pair.
- Capability label/description/icon + strings for theme/notify/inc.

See amethyst/plans/2026-06-23-napplet-nap-theme-notify-inc.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-23 00:23:26 +00:00
Claude c7d876096f feat: NIP-07 window.nostr provider for nSites
nSites now open in "website mode": a normal web app with normal network
access plus a NIP-07 window.nostr provider, so standard Nostr web apps can
"log in with Amethyst" and sign as the active user. Napplets are unchanged
(locked, declared-only sandbox).

- window.nostr (shim.js) installs only when the host sets __nappletNip07
  (website mode). getPublicKey/getRelays reuse the existing consent-gated
  identity reads; signEvent is a new sign-only op honoring the app-supplied
  created_at (no publish — the web app sends to relays itself).
- NappletRequest.SignEvent + nostr.signEvent decode; broker signs as the
  user and returns the signed event without publishing. pubkey is still
  fixed by the signer, so the app can never sign as another identity.
- Website mode: content server defers off-origin requests to the WebView
  and drops the app CSP (normal network); locked napplets keep connect-src
  'none' and 404 off-origin.
- Launcher grants IDENTITY + RELAY (consent-gated) for website mode,
  independent of the nSite's empty manifest requires.
- Consent dialog shows the kind + content preview for a sign request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:49:58 +00:00
Claude 5e4250bfbc fix(napplets): expand the "what it can access" section straight down, not from the left
The disclosure used AnimatedVisibility's default transition and the inner servers
Column didn't fill width, so the section faded/expanded while the server list also
grew in horizontally from the left — an inconsistent, weird effect. Make the
transition explicit (fade + expandVertically/shrinkVertically anchored at Top, so
it opens/closes straight down like the card) and fill width on the servers column
so nothing slides in sideways.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:26:47 +00:00
Vitor PamplonaandClaude Opus 4.8 53d6a2f8b4 fix(napplets/nsites): render real SPAs in the sandbox (blank-page + reload-loop)
Static-site / SPA nApplets & nSites (Vite/nsyte/CRA/webpack output) rendered as a
blank page, then — once that was fixed — as a fast reload-loop blink. Three layered
causes, each found on-device via logcat:

1. Sub-path serving. The applet loaded under https://napplet.local/app/, but bundlers
   emit absolute asset URLs (/assets/app.js, /fonts/…) that resolve against the origin
   ROOT, so every script/style/font 404'd. nSites are defined to be hosted at the domain
   root; serve there.

2. Opaque-origin storage. The applet ran in an `allow-scripts`-only iframe, so its origin
   was opaque ("null"): module scripts + asset fetches were CORS-blocked, and reading
   localStorage/IndexedDB/serviceWorker threw SecurityError — which crash-loops every SPA
   (gruuv: "cache version 0 < 23 → reset → reload", forever, because IndexedDB never
   worked so the version never persisted).

Fix: give each applet its OWN real, persistent, isolated origin — a per-applet subdomain
https://<id>.napplet.local (id = sha256(author:identifier)), framed by the shell with
`allow-scripts allow-same-origin`. A real origin restores localStorage/IndexedDB/SW and
makes the applet's own assets same-origin (no CORS). Isolation is preserved because the
origin is DISTINCT from the shell's: the native bridge stays origin-restricted to the
shell (napplet.local), so the cross-origin applet still can't reach it or read the shell
DOM, and per-applet subdomains keep applets' storage isolated from each other. The shell
HTML's iframe src + CSP frame-src are bound to the specific applet origin at serve time.

Also add an in-memory localStorage/sessionStorage polyfill to the injected shim as
belt-and-suspenders for any context where DOM storage is still unavailable.

By-design sandbox enforcement is unchanged and correctly blocks the rest (external CDN
scripts, direct relay WebSockets via connect-src 'none', external images) — apps must go
through the napplet SDK for those.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:18:56 -04:00
Claude 8c4eae6f5a chore(napplets): rebrand display text to "nApplet" / "nSite"
User-facing strings only — "Napplet(s)" → "nApplet(s)" and the static-site
label → "nSite". Code identifiers, resource keys, CLI verbs (amy napplet/nsite),
and unrelated profile "Website" labels are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 19:52:00 +00:00
Vitor PamplonaandClaude Opus 4.8 d01455f3c9 Merge remote napplet updates into local fix branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:29:24 -04:00
Vitor PamplonaandClaude Opus 4.8 07d25c02db fix(napplet): serialize consent prompts so concurrent requests don't drop
When a napplet issued several consent-gated calls at once (the common case: it
reads relays + storage + identity on load), each launched a NappletConsentActivity
concurrently. The host can only show one, so the rest were delivered to the
single-top activity and silently dropped — their broker calls hung forever
(storage stuck pending; a subscription's consent lost, yielding 0 events).

Gate the consent-prompt path behind a Mutex on the (per-account, reused) broker
so prompts queue one at a time. After taking the lock, re-read the ledger so a
sibling request for the same capability honors the just-recorded grant instead
of prompting again. Only the prompt is serialized — execute() and already-granted
paths stay parallel. Per-use capabilities (payments) still re-prompt every time.

Adds a regression test asserting 5 concurrent same-capability requests yield
exactly one prompt and never two dialogs at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:28:33 -04:00
Vitor PamplonaandClaude Opus 4.8 44deeb1ee0 fix(napplet): launch the sandbox host by reading shell/shim from assets
NappletHostActivity runs in the isolated `:napplet` process, which early-returns
from Amethyst.onCreate to stay key-free and so never initializes the
compose-resources Android context. `Res.readBytes` then threw
MissingResourceException, crashing the host 100% on launch (the napplet feature
could never open).

Read shell.html/shim.js straight from the APK assets (where compose-resources
packages them) via the Activity context instead of the suspending Res accessor.
NappletWebContract now exposes the relative paths + RESOURCE_ASSET_ROOT so the
paths stay single-sourced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:28:32 -04:00
Claude a2eae42c07 feat(napplets): app-store-style card + icon manifest tag; demote capabilities
Redesign the shared StaticWebsiteCard (used by the feed AND the napplets browse
screen) to look like an app entry instead of a manifest dump: square app icon
(with a colored monogram fallback), name, a NAPPLET/WEBSITE type label, a short
description, and an Open button. The technical details users don't care about —
declared capabilities, Blossom servers, source URL — move behind a tap-to-expand
"What it can access" disclosure; capabilities are still re-confirmed at the
consent prompt when actually used and remain fully manageable in the permissions
screen.

Add an `icon` tag (NIP-5A/5D) end-to-end:
- quartz: IconTag + siteIcon() accessor/builder, NappletManifest.icon(), and an
  icon param on all four site/napplet build() factories (+ round-trip test).
- amy: `--icon URL` on `nsite/napplet publish`, surfaced in the publish output.
- card: renders the icon via Coil, monogram fallback when absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 17:13:41 +00:00
Claude ece1f43975 feat(amy): publish napplets and nsites (ship a directory)
Add `amy nsite publish <dir>` and `amy napplet publish <dir>` so a static-site
or napplet directory can be shipped to Nostr in one command, building on the
new CLI/Blossom infrastructure.

- commons (jvmMain) StaticSitePublisher: the reusable "upload a tree" half —
  walks a directory (or single file), content-addresses each file, BUD-02
  signed-uploads it via BlossomClient, and maps it to an absolute web path
  (/index.html, /assets/app.js, …). Returns the NIP-5A path→sha256 tags.
- cli StaticSitePublish: thin shared flow — uploads via the commons publisher,
  hands the path tags to a kind-specific builder, signs with the account key,
  and broadcasts. nsite builds 15128/35128 (+ x aggregate); napplet builds
  15129/35129 (aggregate + requires already added by the quartz builder).
- nsite/napplet `publish` verbs wired into their routers.

Test harness README now recommends `amy napplet publish tools/napplet-test`,
keeping publish.sh as a no-amy fallback. Unit test covers the path mapping;
cli + commons build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 15:31:28 +00:00
Claude 08947510d9 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad 2026-06-22 15:04:44 +00:00
Claude a5eabbf1a2 Merge remote-tracking branch 'origin/main' into claude/loving-hopper-rn553t 2026-06-22 14:53:23 +00:00
Claude df15414424 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad
# Conflicts:
#	amethyst/src/main/res/values/strings.xml
2026-06-22 14:50:16 +00:00
ClaudeandVitor Pamplona 4e582d198d fix(nutzaps): receive nutzaps on inbox/dm/kind:10019 relays, not outbox
Inbound NIP-61 nutzaps (kind:9321) are messages other people send *to*
the user, so per the NIP-65 outbox model they must be read from the
user's inbox-side relays, not their outbox. The Cashu subscription used a
single relay set (outbox) for both the user's own NIP-60 events and
inbound nutzaps, so a sender following NIP-61 correctly (publishing to
the relays advertised in the recipient's kind:10019, or to the
recipient's NIP-65 inbox) could be missed.

Split the subscription relay sets per filter:
  - own NIP-60 wallet/token/history events keep reading from outbox,
    where the user published them (needed to restore on a fresh device);
  - inbound kind:9321 nutzaps now read from the union of the user's
    NIP-65 inbox + DM relays + the `relay` tags in the user's own
    kind:10019. The last one is NIP-61's source of truth for "where to
    send me nutzaps" and may be written by another client to a relay set
    unrelated to our NIP-65 lists, so we listen there too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHcZ2gv8ro9Q2bEiTSiHD8
2026-06-22 09:38:31 -04:00
Claude e883d99f53 feat(napplet): implement keys.onAction (key binding + keys.action push)
Bind a napplet's registered keyboard/command actions to real hardware-key
combos and fire them back as keys.action pushes, so napplet.keys.onAction
actually triggers (previously registration was acked but never fired):

- protocol: RegisterAction / ActionRegistered carry the key combo (binding,
  from the SDK's action.defaultKey); the codec decodes defaultKey and echoes
  binding in the result; encodeKeysAction push envelope added.
- broker: registerAction returns the honored binding (still no key access for
  the applet; KEYS stays a declared-only, no-prompt capability).
- NappletKeyActions (host): a registry that parses combos like "Ctrl+Shift+S"
  / "Cmd+Enter" / "F2" and matches them against KeyEvents.
- NappletHostActivity: binds an action only after the broker authorizes it
  (from the keys.registerAction.result), unbinds on keys.unregisterAction, and
  overrides dispatchKeyEvent to turn a matching combo into a keys.action push
  via the shell bridge. Unmatched keys fall through to the WebView, so the
  applet's own text inputs keep working. Touch-only devices simply never match.

shim already passed the full action (incl. defaultKey) and wired onAction to
the keys.action push, so no shim change was needed. Conformance test now
covers the defaultKey decode + binding round-trip; all napplet suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 01:37:39 +00:00
Claude 1a1fb9ff32 feat(napplet): implement identity.onChanged push
Wire napplet.identity.onChanged end to end so an applet is notified when the
active user's public key changes (account switch / connect / disconnect):

- shim: onChanged registers a handler and opens a watch (identity.watch) on the
  first handler; closing the last one stops it (identity.unwatch). identity.changed
  pushes are dispatched to the handlers with the new pubkey.
- router: identity.watch (gated on the IDENTITY declaration) / identity.unwatch
  become WatchIdentity / UnwatchIdentity outcomes — a push subscription, like
  relay.subscribe, that never reaches the broker.
- NappletIdentityWatch (host): collects the active account's pubkey from the
  session manager and pushes identity.changed on each subsequent change (the
  current value is dropped — the applet already has it via getPublicKey). Torn
  down on unwatch and on service destroy.
- codec: encodeIdentityChanged push envelope.

Router unit tests cover watch (declared/undeclared) and unwatch; commons tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 01:32:07 +00:00
Claude 443c35762a feat(napplet): implement identity.getList/getZaps/getBadges + resource nostr:
Fill in the identity reads that previously fell through to Unsupported, and
add nostr: resolution to resource.bytes — both reading from what Amethyst
already has locally, matching the @napplet/nap shapes.

identity (AccountIdentityReader):
- getList(listType): public tag values (e/a/p/t/word/r/emoji) of the user's
  NIP-51 replaceable list of that type (bookmarks 10003, pins 10001, mute
  10000, interests 10015, communities 10004, channels 10005, emojis 10030).
- getZaps: ZapReceipt[] {eventId, sender, amount, content?} from kind-9735
  receipts p-tagging the user in the cache.
- getBadges: Badge[] {id, name?, description?, image?, thumbs?, awardedBy}
  from kind-8 awards p-tagging the user, resolved against their kind-30009
  definitions in the cache.

resource.bytes (NappletResourceFetcher):
- nostr: URIs (NIP-19) resolve to the referenced event's JSON
  (application/json). nembed carries it inline; note/nevent/naddr resolve
  from the cache then a bounded relay fetch; npub/nprofile resolve the
  author's kind-0. Still no direct network for the applet.

The wire codec already routed these (generic IdentityRead + identityResultField,
ResourceBytes), so no protocol change was needed. Commons napplet tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 01:27:53 +00:00
Claude 47cc76d9f1 feat(cli): amy admin (NIP-86) + serve (embed geode relay)
Two new nak-parity commands:

- `amy admin RELAY METHOD [args]` — NIP-86 Relay Management API over NIP-98
  HTTP auth. Full method set: ban/unban + allow/unallow pubkey, ban/allow
  event, allow/disallow kind, block/unblock IP, change name/description/icon,
  and all list-* queries. Reuses quartz's Nip86Client (request build + NIP-98
  auth + parse) and the Nip86Retriever HTTP path — extracted from amethyst to
  commons/jvmAndroid so amy and the Android relay-management screen share it.

- `amy serve [--host --port --path --db --admin]` — runs a Nostr relay by
  embedding geode (the standalone Ktor relay on quartz's relay-server code).
  In-memory by default (ephemeral, like nak serve); --db FILE for SQLite. The
  account's own pubkey is always an admin, so `amy admin` works against it out
  of the box. cli gains a :geode dependency (geode depends only on :quartz) and
  kotlinx-serialization-json (to render NIP-86 JSON results).

Verified end-to-end: `amy serve` + `amy admin ws://127.0.0.1:PORT
supported-methods|change-name|ban-pubkey|list-banned-pubkeys` round-trip
cleanly over real HTTP + NIP-98 against the live geode relay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 00:19:29 +00:00
Claude 42a82c7e8b feat(cli): amy cashu receive/send/maintenance/mint-rec tiers
Complete the Cashu command surface, every verb a thin wrapper over the
shared commons CashuWalletOps the Android wallet runs:

  cashu receive ln SATS [--mint]      start mint, return bolt11 + kind:7374
  cashu receive complete QUOTE_ID     poll mint; on settle, mint proofs
  cashu receive resume QUOTE_ID       alias of complete
  cashu receive token TOKEN           redeem a cashuB token
  cashu receive nutzap-sweep [--mint] redeem inbound NIP-61 nutzaps
  cashu send ln INVOICE [--mint]      melt to a bolt11 (scrubs first)
  cashu send token SATS [--mint --memo]  export a cashuB token (scrubs first)
  cashu send nutzap USER SATS [--zapped --message]  P2PK-locked nutzap
  cashu maintenance scrub [--mint]    NUT-07 + NIP-09 prune spent proofs
  cashu maintenance restore MINT_URL  NUT-09 restore from seed
  cashu maintenance migrate-keysets [--mint]  consolidate onto active keyset
  cashu mint-rec show [--author] / add URL [--dtag --review] / remove ID

- scrubStaleProofs extracted into CashuWalletOps so Android's
  CashuWalletState.scrubLocallyStaleProofs and amy share one impl.
- Context.cashuRestore mirrors CashuWalletState.restoreFromMint (seed +
  NUT-13 counter bump); Context.cashuSeed warms the per-run seed.
- receive complete recovers the mint amount by decoding the quote's
  bolt11 (kind:7374 stores only the quote id), so it works statelessly.

Verified against mint.minibits.cash + live relays: receive ln returns a
real invoice, complete/resume poll a pending quote, mint-rec round-trips,
and every error path (insufficient_funds, no_mint, mint_quote_gone) is
clean. Happy-path mint/melt completions need a payable bolt11 (interop
harness, PR 9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 00:10:40 +00:00
Claude b6f9630883 refactor(napplet): single-source the web contract and feed card in commons
Two shared extractions so the future desktop host reuses the exact same
sandbox and feed UI as Android, with no chance of drift:

Shared web contract:
- Move shell.html + shim.js into commons composeResources
  (files/napplet/), read via Res.readBytes on any platform.
- New NappletWebContract (commons/commonMain) single-sources the whole
  web contract: the shell/shim loaders plus the internal origin/host/URLs
  and both Content-Security-Policies (SHELL_CSP, APP_CSP). The Android
  host preloads the bytes in onCreate and reads every origin/CSP constant
  from NappletWebContract instead of its own duplicated constants and
  assets.open() calls.

Shared feed card:
- New StaticWebsiteCard (commons/.../ui/note) renders the inert NIP-5A /
  NIP-5D preview card: self-contained with commons compose-resource
  strings, LocalUriHandler for links, and inlined card chrome. It takes
  an isNapplet flag and an onOpen launch slot, so it never executes applet
  code itself.
- amethyst's note/types/StaticWebsite.kt becomes thin event->card
  adapters that supply the sandboxed onOpen launch.

Card strings move to commons strings.xml. Both napplet test suites
(commons jvmTest + amethyst) stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 00:10:22 +00:00
Claude 0cff3bf8e2 refactor(napplet): extract host-agnostic NappletRequestRouter to commons
Move the decode → broker → encode orchestration out of Android's
NappletBrokerService.handleMessage and into a pure, transport-free
NappletRequestRouter in commons/jvmAndroid. It returns a small Outcome
(Ignore / Reply / OpenSubscription / CloseSubscription / Push) that each
host acts on, so the Android service and the future desktop host share
the routing brain and can't drift on wire behavior.

The service now resolves the broker and dispatches on the Outcome,
supplying only the Messenger transport and the live relay subscription.
openLiveSubscription takes the decoded filters from the router instead of
re-decoding the payload, and the now-redundant process() is removed.

Unit-tested in commons/jvmTest (NappletRequestRouterTest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 23:59:53 +00:00
Claude 8bb537438f feat(cli): amy cashu wallet/mint/balance on shared NIP-60/61 code
Add the offline Cashu command tier to amy, all driven by the shared
commons wallet code so amy exercises the same path as the Android app:

  amy cashu wallet create [--mint URL] [--mints a,b] [--privkey HEX] [--relay r1,r2]
  amy cashu wallet show
  amy cashu wallet export-key
  amy cashu wallet destroy
  amy cashu mint ping URL          (stateless)
  amy cashu mint info URL          (stateless)
  amy cashu balance [--mint URL]

- create/destroy reuse commons CashuWalletOps.publishWalletEvents /
  deleteWallet; show/balance reuse the CashuWalletReader projection over
  the local event store; mint ping/info hit quartz's MintHttpClient.
- Context gains cashuOps() (wired to the file NUT-13 counter store + a
  per-run seed cache) and cashuSnapshot(); DataDir gains cashu.json.
- Extraction D: CashuKeysetCounterStore contract in commons +
  FileCashuKeysetCounterStore (atomic ~/.amy/<account>/cashu.json).

PRs 4 of cli/plans/2026-05-28-cashu-cli.md (extractions A–D + offline
tier). receive/send/maintenance/mint-rec + interop harness still pending.
Verified end-to-end against mint.minibits.cash and live relays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:30:59 +00:00
Claude d111c2589d refactor(commons): extract CashuWalletReader projection for amy reuse
Add a pure, stateless CashuWalletReader in commons that projects a stream
of NIP-60/61/87 events into a WalletSnapshot (wallet/nutzap-info events,
decrypted mints, unspent token entries, history, pending quotes, nutzaps,
recommendations, plus balance + per-mint balances).

Android's CashuWalletState keeps its incremental dirty-tracking and
StateFlow plumbing but now delegates the two tricky computations —
del-rollover over decrypted tokens (computeUnspent) and the
destroyed/expired pending-quote filter (computePending) — to the shared
reader instead of carrying its own copies. amy will call project() once
per command over its event store.

Extraction C of cli/plans/2026-05-28-cashu-cli.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:20:25 +00:00
Claude fc7db088b2 refactor(commons): extract CashuWalletOps to commons for amy reuse
Move the NIP-60/61 wallet orchestration layer (CashuWalletOps + its
result types: TokenEntry, MintQuoteStarted, MintCompleted, MeltCompleted,
SendTokenCompleted, RedeemCompleted, NutzapSent, RestoreOutcome,
MigrationResult, CreatedWallet, describeMintError) out of amethyst into
commons/jvmAndroid/cashu/ops.

The class already had zero Android dependencies — it takes signer,
publish, okHttpClient, secretFactory, and the NUT-13 counter callbacks as
constructor params. It lands in the jvmAndroid source set (not commonMain)
because it composes quartz's jvmAndroid CashuMintOperations/MintHttpClient
and uses ConcurrentHashMap. Both Android (amethyst) and the JVM CLI (amy)
can now drive the exact same wallet code path.

This is Extraction B of cli/plans/2026-05-28-cashu-cli.md. Android
callers (CashuWalletState, the wallet ViewModels, AccountViewModel)
updated to the new package; no behavioral change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:17:04 +00:00
Claude 0b76518ef2 refactor(napplet): move wire codec to commons for desktop reuse + desktop host plan
Set the desktopApp up to host napplets/nsites by maximizing the shared
core and documenting the edge it must build.

- Moved NappletProtocolJson (the wire codec) from amethyst to
  commons/jvmAndroid (package ...commons.napplet.protocol), next to the
  NappletRequest/Response types it marshals. It depends only on quartz +
  kotlinx.serialization + java.util.Base64 (Android 26+/JVM), so a future
  desktop host marshals through the identical object — request/result/push
  shapes can't drift between platforms. amethyst host/service/tests updated
  to import it; tests stay in amethyst and still exercise it.
- Added desktopApp/plans/2026-06-21-napplet-desktop-host.md: what's already
  shared (broker, protocol, codec, resolver, the shell.html/shim.js web
  contract), what desktop must build (KCEF/JCEF engine, custom-scheme
  serving, isolation, transport, gateways, UI), the decisions to make, a
  security-parity checklist, and recommended further extractions
  (NappletRequestRouter, shared web assets, the inert feed card).

commons:jvmTest and the amethyst napplet suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 22:12:28 +00:00
Claude cd2bae081e feat(napplet): live subscription tail, multi-filters, resource.cancel
Implements the remaining tractable conformance follow-ups:

- Live subscription tail: relay.subscribe now opens a real
  client.subscribe whose SubscriptionListener streams relay.event
  (stored + live), relay.eose, and relay.closed pushes keyed by subId,
  instead of a one-shot snapshot. relay.close unsubscribes (tracked in
  liveSubs, torn down in onDestroy). The broker only authorizes the
  subscription (RELAY consent) and returns Subscribed; the host owns the
  live stream. Shim dispatches relay.closed too.
- Multi-filters: relay.query/subscribe honor every filter in filters[],
  not just the first (decodeFilterList; gateway query(List<Filter>);
  queryEvents unions across filters; max limit applied).
- resource.cancel: accepted at the host edge as a no-op Done.

Conformance tests extended (multi-filter decode, relay.closed push);
commons:jvmTest and the amethyst napplet suite pass.

Still open: identity getList/getZaps/getBadges + onChanged (shapes
underspecified), the keys.action push (needs a host trigger UI), the
resource nostr: scheme, inc + the niche domains, and on-device
verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 21:43:36 +00:00
Claude 0f23051d54 feat(napplet): implement the four SDK conformance breakers
Closes the 🔴 items from the conformance audit so stock @napplet/shim
napplets interop:

1. Shell handshake: the host answers shell.ready with shell.init
   {capabilities:{domains,protocols},services} built from the declared
   domains (NappletProtocolJson.encodeShellInit), so supports() works.
2. Id-less messages: onShellMessage no longer drops messages without an
   id — shell.ready is answered locally and fire-and-forget messages get
   a synthetic id so they reach the broker.
3. keys: keys.registerAction/unregisterAction decode and the broker acks
   them (declared-gated, no consent) so registerAction() resolves; the
   shim dispatches the keys.action push. (Global-key binding is a
   follow-up — keys.action isn't emitted yet.)
4. upload: realigned to upload.upload{request:{data,mimeType,filename}} →
   rich UploadResult{ok,uploadId,status,url,sha256,size,mimeType};
   shell.html inlines the request Blob as base64 so it survives the
   bridge; the gateway uploads via the app's BlossomUploader to the
   user's kind:10063 server with a signed auth event.

NappletSdkConformanceTest's gap guards flip to conformance assertions for
shell.init/keys/upload; inc stays the one documented gap. commons:jvmTest
and the amethyst napplet suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 21:02:55 +00:00
Vitor PamplonaandClaude Opus 4.8 df1982a7f6 feat(feeds): prefetch media + pre-parse text ahead of the viewport
Warms the next/previous few feed notes off the main thread so media and
link previews are ready before the user scrolls to them, and pre-parses
rich-text bodies into the shared cache so scroll-time composition is a
cache hit instead of a UI-thread parse.

Per upcoming note (off Dispatchers.Default, deduped via a per-feed set,
cancelled on a new visible range):
- Pre-parses TextNote/Comment bodies through CachedRichTextParser using the
  renderer's exact key, so the composition reads a cached parse. Other kinds
  still get their media/links discovered, just without the render-cache warm.
- Prefetches images into Coil — inline content images, NIP-92 imeta blobs,
  NIP-94 file-header url tags, and video poster frames — and records each
  decoded aspect ratio in MediaAspectRatioCache so the box is reserved on
  first layout (no jump). Video poster ratios seed the video URL's box too.
- Warms OpenGraph/link previews via UrlCachedPreviewer.
Gated on showImages()/showUrlPreview() so it honors data-saver/Wi-Fi-only.
Video bytes are deliberately not prefetched (large, HLS, player pool already
starts fast).

Wired centrally at the two feed dispatchers (RenderFeedContentState,
RenderFeedState) so every list feed routed through them is covered without
per-screen wiring, plus direct hooks for the custom-render feeds (hashtag,
profile notes) and a LazyGridState variant for the grid feeds (gallery,
products, discover).

CachedRichTextParser is now content-addressed (memoized contentHash on
ImmutableListOfLists) so an off-thread pre-parse maps to the same entry the
renderer looks up, and its cache grows 50 -> 500 to hold the prefetch +
multi-feed working set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:44:39 -04:00
Claude 24638cc50c feat(napplet): implement the identity read API
Wire the upstream identity.* read methods (beyond getPublicKey) to the
active Account, returning JSON gated by the IDENTITY consent:

- getProfile  -> kind-0 metadata content
- getRelays   -> NIP-65 { "<url>": { read, write } } map
- getFollows  -> kind-3 followed author pubkeys
- getMutes    -> NIP-51 mute-list user pubkeys (decrypted)
- getBlocked  -> NIP-51 block-list user pubkeys (decrypted)

getList/getZaps/getBadges route through but degrade to Unsupported for
now; onChanged stays a client-side no-op until the live push channel
lands. Reads are public data only — never key material — and remote/
external signers still self-gate the consent.

Adds NappletRequest.IdentityRead, NappletResponse.Json, a
NappletIdentityGateway collaborator, codec round-trip for identity.*,
the shim methods, and unit tests. commons:jvmTest and the amethyst codec
test pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 14:26:27 +00:00
Claude b43c1c9eda fix(napplet): align shell to verified @napplet/shim 0.15+ contract
Reverse-engineered the authoritative @napplet/shim (npm v0.16.0) and
corrected the napplet host to its real wire contract. The prior commit
carried guessed method names that would break real ecosystem napplets.

- Signing model: napplets never get a sign() (per upstream "signing and
  encryption are mediated by the shell"). Dropped keys.signEvent /
  keys.nip04* / keys.nip44*. relay.publish now takes an UNSIGNED template
  and the broker signs it as the user, returning the signed event;
  added relay.publishEncrypted (shell encrypts + signs + publishes).
- keys -> keyboard/command actions (registerAction/unregisterAction/
  onAction), client-side no-op stubs (not yet wired to the host keyboard).
- storage: get/set/remove -> getItem/setItem/removeItem; added storage.keys
  end-to-end (protocol, broker, DataStore, shim).
- resource.bytes returns a Blob (shim builds from {bytes, mime}).
- shell.supports gains optional protocol arg; added shell.ready/onReady/
  services stubs.
- relay.subscribe wired (initial matches; live tail still a follow-up).
- value.payInvoice and upload.blob kept as clearly-marked Amethyst-specific
  extensions (no upstream equivalent; real napplets never call them).

Broker now defers per-signature consent to remote/external signers via a
signsAsUser flag (publish/publishEncrypted) instead of an IDENTITY/KEYS
capability check. Tests updated; commons:jvmTest and the amethyst codec
round-trip test pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 19:39:36 +00:00
Claude 5ca44e277f feat(napplet): align with the upstream napplet SDK (envelope, namespaced API, domains)
Acts on the ecosystem audit so real napplets built against @napplet/web can run.

Wire: switch to the upstream envelope {type:"<domain>.<action>", id} →
{type:"…​.result", id, ok, …} across the JS shim, NappletProtocolJson, and the host
shuttle (host forwards the verbatim envelope, injects id on the reply).

API: rewrite the injected window.napplet.* to the namespaced SDK surface —
shell.supports, identity.getPublicKey (+onChanged stub), keys.{signEvent,nip04*,
nip44*}, relay.{publish,query,subscribe}, storage.{get,set,remove}, value.payInvoice,
resource.{bytes,bytesAsObjectURL}, upload.blob. subscribe currently returns the
initial matches via query (live tail is a follow-up).

Capabilities: split to the domain model — SHELL, IDENTITY, KEYS, RELAY, STORAGE,
VALUE, RESOURCE, UPLOAD (was IDENTITY/RELAY/WALLET/STORAGE/NET). shell.supports is
answered with no consent, reflecting declared+brokered domains; keys (signing) split
from identity (pubkey); signer-self-gating now covers both.

New ops: resource.bytes (https/data, broker-fetched, Tor-routed, consent-gated).
upload is wired end-to-end but its Android Blossom gateway is left unprovided
(Unsupported) pending the Uri + auth-event + server-selection integration.

Codec moved to java.util.Base64 (real on minSdk 26 and in JVM tests). Permissions
screen + capability labels updated to the 8 domains; new consent/label strings
localized. Broker + codec + capability tests updated/added.

:commons:jvmTest, :amethyst:testPlayDebugUnitTest (codec), and
:amethyst:compileFdroidDebugKotlin pass; spotless clean. See
plans/2026-06-20-napplet-ecosystem-audit.md (Update section) for the remaining gaps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 16:54:06 +00:00
Claude c999d2ac73 feat(napplet): permissions management screen
A modern Material3 screen to review and revoke the permissions napplets hold.

Data layer (commons, tested): NappletPermissionStore gains all() (enumerate
persisted grants by coordinate) and remove(coordinate, capability); the ledger
gains allPersistedGrants() and revoke(identity, capability). DataStore actual
implements both (capability is the final space-delimited token of each key).

UI (amethyst): NappletPermissionsScreen renders one ElevatedCard per napplet —
resolved title + author, and a row per capability with an icon, label, and a
control: a Switch (Allowed/Blocked) for normal capabilities, or a "Blocked"
indicator for per-use ones (payments only ever persist a DENY). Each row has a
revoke action; each card a "Forget this napplet" (revokeAll). Empty state with a
shield. Reads/writes the same DataStore the broker uses, so changes take effect
immediately. Reached via a "Manage permissions" action on the Napplets top bar
(Route.NappletPermissions).

commons ledger tests added for allPersistedGrants + single-capability revoke.
:commons:jvmTest and :amethyst:compileFdroidDebugKotlin pass; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 15:28:28 +00:00
Claude 916a624ddb feat(napplet): capability-aware consent — per-payment, signer-aware identity, foreground-only
Refines the uniform consent model now that wallet + identity carry different risk.

Payments (WALLET): NappletCapability.requiresPerUseConsent — every payInvoice
re-prompts (with the decoded sats amount); the dialog drops "Always allow" and the
broker downgrades any always/session grant to one-shot, so a payment grant is
never persisted. No silent spend.

Identity: gated by us only when Amethyst holds the key (NostrSignerInternal). For
remote (NIP-46) / external (NIP-55) signers the broker defers to the signer's own
per-request consent instead of double-prompting — while still honoring a standing
per-napplet DENY and the requires declaration. The sign prompt shows a kind +
content preview.

Foreground-only execution: NappletHostActivity pauses the WebView's JS/timers in
onPause and resumes in onResume, so a backgrounded applet can't fire a
sign/decrypt/pay request whose prompt would be confused with Amethyst's own UI.
This is the precondition that makes deferring identity to an external signer safe.

commons broker tests added: wallet prompts every time and is never persisted;
external signer defers identity without prompting but still honors a standing DENY.

:commons:jvmTest and :amethyst:compileFdroidDebugKotlin pass; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 15:15:10 +00:00
Claude b422274140 feat(napplet): capability enforcement, read/storage capabilities, nsite host wiring
Closes the highest-leverage gaps from the completeness report (items 2–5).

Capability enforcement (#2):
- The broker now refuses any request whose capability is not in the manifest's
  `requires`. The host resolves `requires` to a declared capability set and sends
  it with every IPC request; the broker denies undeclared capabilities before any
  consent prompt. Per-operation consent summaries added for the new ops.

Read capability (#3):
- New QueryEvents request (RELAY capability) → NappletRelayGateway.query, answered
  from LocalCache (account.cache.filter). window.napplet.queryEvents(filter) added.

nsite host wiring (#4):
- NappletLauncher generalized to launch any NIP-5A site from paths+servers, so
  nsites (kinds 15128/35128) open in the sandbox too. The nsite card
  (StaticWebsite) gets an "Open" button; nsites declare no capabilities, so the
  broker refuses everything and they render as inert static content.

Storage + wallet (#5):
- STORAGE fully implemented: StorageGet/Set/Remove + DataStoreNappletStorage,
  namespaced per applet coordinate. window.napplet.storage.{get,set,remove}.
- WALLET modeled with PayInvoice + NappletWalletGateway, but kept Unsupported (no
  gateway provided) — no money path ships until verified end-to-end.
- Inter-applet messaging and live (non-cache) relay query remain v2.

commons broker tests cover declaration enforcement, query, and storage round-trip.
:commons:jvmTest and :amethyst:compileFdroidDebugKotlin pass; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 13:50:19 +00:00
Claude da336897c2 fix: keep balanced closing delimiters and inline commas in detected URLs
The URL detector stripped a single trailing punctuation char unconditionally,
which dropped the closing ")" from legitimate URLs such as
https://en.wikipedia.org/wiki/Bitcoin_(disambiguation).

Make the trailing strip balance-aware: a trailing ")", "}" or "]" is kept when
the URL contains its matching opener (balanced), and only stripped when it is
unbalanced wrapping/sentence punctuation (e.g. "(see example.com)" or
"http://test.com)"). Commas without surrounding spaces were already kept inside
paths; this also adds "]" to the begin/end punctuation sets so an unbalanced
bracket is handled symmetrically with parens and braces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzZzVcMcuzjSdhD3xqCE87
2026-06-19 23:38:44 +00:00
Claude 119442293a feat(napplet): trust-boundary core for sandboxed nsite/napplet rendering
Adds the platform-agnostic core for hosting untrusted napplet (NIP-5D) /
nsite (NIP-5A) web content behind a hard trust boundary, so applet HTML/JS
can never reach the nsec, app storage, or LocalCache.

The Android host runs the WebView in a separate OS process (:napplet) that
holds no secrets and brokers every dangerous operation over IPC to the main
process. This commit lands the verifiable heart of that boundary in commons
commonMain (KMP-pure, fully unit-tested):

- NappletCapability + NAP-domain mapping (default-deny on unknown domains)
- NappletIdentity keyed by addressable coordinate (grants survive updates)
- NappletPermissionLedger / GrantState / store (persistent vs session vs once;
  standing DENY is authoritative)
- NappletRequest/NappletResponse wire protocol (no response carries key bytes)
- NappletBroker: the only holder of the signer; enforces consent, signs as the
  user only, refuses to publish foreign or unsigned events

Architecture, process model, IPC schema, WebView hardening, and consent UX are
documented in amethyst/plans/2026-06-19-napplet-sandbox-host.md. The Android
:napplet process, WebView host, AIDL broker, and consent UI are the next phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-19 23:22:45 +00:00
Claude eac0daed9c refactor(blossom): centralize Blossom protocol strings in quartz
The Blossom auth-header encoding (`Nostr <base64-event>`), the `/upload`
endpoint path, and the `X-Reason` failure header were each re-derived in
both the commons JVM `BlossomClient`/`BlossomAuth` and the Android
`BlossomUploader`, using two different Base64 APIs. Move these
protocol-level facts into the quartz `nipB7Blossom` package, where the
rest of the Blossom protocol lives:

- `BlossomAuthorizationEvent.toAuthorizationHeader()` / `rawToken()` +
  `AUTH_HEADER_SCHEME`, mirroring NIP-98's
  `HTTPAuthorizationEvent.toAuthToken()` that Blossom auth reuses.
- new `BlossomServerUrl` with `upload()` / `blob()` endpoint builders and
  the `REASON_HEADER` constant.

Both transports now call these helpers instead of hand-building strings.
No behavior change for upload (existing desktop BlossomClientTest still
green); the Android delete URL now omits the trailing dot when no file
extension is known, matching BUD-02's `DELETE /<sha256>`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJgwV4Y99brVa97v7p3jJb
2026-06-19 22:40:28 +00:00
Claude 1b2311e75f feat(cli): add amy nsite fetch to resolve + verify static sites / napplets
Wires the quartz NIP-5A resolver end-to-end so it can be exercised against
real manifests (interop / agents), without building the security-sensitive
WebView shell yet.

- commons BlossomClient: add download(url) — a Blossom GET returning raw bytes
  (null on non-2xx; connection failures propagate so callers try the next
  server). Does not verify the hash; that is the resolver's job.
- cli NsiteCommands: `amy nsite fetch AUTHOR [--d ID] [--path P] [--server …]
  [--relay …] [--out FILE] [--timeout SECS] [--max-inline-bytes N]`. Fetches
  the manifest (kind 15128 root, or 35128 named with --d) from relays, then
  resolves one path through StaticSiteResolver, downloading from the manifest's
  Blossom servers (plus any --server fallbacks) and accepting only the first
  blob whose sha256 matches the manifest pin. Emits the verified path's bytes
  (inlined for small text, or written to --out) with hash/server/content-type,
  or a structured not_found / path_not_found / unresolvable error.

Thin-assembly only: all resolution + verification stays in quartz, the byte
fetch in commons. Smoke-tested offline: bad-args, help, and a dead-relay run
that resolves cleanly to not_found in both text and --json modes.

Also converts the StaticSitePathLookup file-overview KDoc to a plain block
comment to satisfy ktlint no-consecutive-comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 20:46:32 +00:00
Claude c5b1d0051a refactor(cashu): move out-of-band token parser + model to quartz
Follow-up to the V4Encoder move. Quartz could encode a cashuB string but
could not parse cashuA/cashuB back, and the parsing lived in amethyst even
though it is pure NUT-00 wire-format protocol. Worse, commons.RichTextParser
already detects cashuA/cashuB words while the parser sat up in the app, so
Desktop (its own rich-text viewer) could not parse a received token at all.

Consolidate the legacy out-of-band redeem stack onto quartz:

- new quartz CashuTokenB64Parser parses cashuA (standard-Base64 JSON, rewritten
  off Jackson onto kotlinx.serialization to satisfy quartz's no-Jackson rule)
  and cashuB (Base64URL CBOR, reusing the V4Token models), returning quartz
  types. It is the inverse of V4Encoder.
- move the CashuToken container model from commons to quartz, switching its
  proofs from the duplicate commons Proof (field C, amount Int) onto the
  canonical quartz CashuProof (field c, amount Long). The duplicate Proof
  type is deleted.
- delete amethyst V3Parser/V3Token/V4Parser; CashuParser/CachedCashuParser
  stay as thin amethyst adapters (off-main-thread guard + GenericLoadable +
  LruCache) over the quartz parser.
- this removes the manual Proof -> CashuProof conversion shims that
  MeltProcessor and CashuWalletViewModel previously carried.

Tests: full cashuA + cashuB vector coverage moves to quartz commonTest
(CashuTokenB64ParserTest, runs on JVM) plus an encode/parse round-trip; the
superseded amethyst CashuV4ParserTest is removed and CashuBTest stays as the
adapter integration test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
2026-06-19 15:32:48 +00:00
Róbert NagyandGitHub 5e3fea94a1 Merge branch 'main' into feat/desktop-relay-latency-health 2026-06-19 10:28:07 +03:00
Claude bb1d034be9 Merge remote-tracking branch 'origin/main' into claude/thread-note-collapse-zwnjqb 2026-06-18 20:32:05 +00:00
davotoulaandClaude Opus 4.8 2a7493019b fix: remove literal backslash in DM relay-incomplete label
Compose Multiplatform string resources don't use Android res/values
escaping, so \' rendered literally as didn\'t on the DM history card.
Use a plain apostrophe to match the sibling new_key_continue_button string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:25:34 +02:00
Vitor PamplonaandGitHub 3948c8dbd1 Merge pull request #3266 from vitorpamplona/claude/relay-connections-background-03x58e
Fix lifecycle-aware subscriptions and notification relay throttling
2026-06-18 15:42:28 -04:00
Claude 738589fe2f chore(relay): remove diagnostic logging, restore 30s unsubscribe grace
Strips the BgRelayTrace instrumentation added while diagnosing the
background relay-count issues and restores the production grace period.

- LifecycleAwareKeyDataSourceSubscription: UNSUBSCRIBE_GRACE_MILLIS back to
  30s, drop the per-subscription label + logs, refresh the doc to describe
  the LifecycleEventObserver detection.
- RelayPool: drop updatePool trace logs and the now-unused Log import; keep
  the _connectedRelays prune (with a trimmed comment).
- BaseEoseManager: drop the per-assembler relay-count log + Log import.
- SubscriptionController: drop activeRelays(), which only fed that log.

The actual fixes stay: lifecycle-observer teardown detection, the
connected-set prune, and the notification-count throttle + fg/bg wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukw6FJPFh3JKGXL532p3ae
2026-06-18 18:15:32 +00:00
Claude 840868d315 fix: fall back to observed relays when public chat declares an empty relay list
PublicChatChannel.relays() was `info.relays?.toSet() ?: super.relays()`.
An empty (non-null) declared-relay list — `emptyList()?.toSet()` — yields an
empty set and short-circuits the elvis, so the channel reported zero relays
instead of falling back to the relays it was actually observed on. Both the
message-send path and the broadcast path (computeRelaysForChannels /
wantsBroadcastRelays) read relays(), so the message was published to nowhere
while a manual broadcast still reached the user's personal relays — matching
the reported symptom.

Treat an empty declared list like "no declared relays" via ifEmpty, so it
falls back to observed relays. Adds PublicChatChannelRelayTest covering the
declared-relay round-trip, message->channel resolution, and the empty-list
fallback regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AYtHYEob2THu74inTxZxCh
2026-06-18 15:42:52 +00:00