Commit Graph
15393 Commits
Author SHA1 Message Date
Claude 56d836724f feat(napplet): structured-clone transport + relay.subscribe push channel
Stock napplets (built on @napplet/core) post structured-clone OBJECTS,
not JSON strings, and expect object replies — our shell only forwarded
strings, so their messages were dropped. Bridge the transport so real
napplets interop:

- shell.html bridges object<->string both directions: applet->native
  serializes object envelopes; native->applet parses and posts a
  structured-clone object (what the SDK reads via e.data.type).
- resource.bytes returns a real Blob: the shell rebuilds it from the
  host's base64 bytes+mime before delivering.
- relay.subscribe is push-based: relay.event (per match) then relay.eose,
  keyed by subId, no .result — matching @napplet/shim. New MSG_PUSH IPC
  frame carries unsolicited envelopes the host forwards verbatim;
  relay.close is a fire-and-forget no-op. Delivers the initial snapshot
  then EOSE (a live tail is a follow-up).

Injected shim updated to accept object messages, dispatch subscription
pushes by subId, and use the push-based subscribe. Codec gains
encodeRelayEvent/encodeRelayEose/readSubId with unit tests.

The shell/shim are JS and not covered by the JVM tests — this needs
on-device verification with a playground napplet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 15:34:36 +00:00
Claude 17b3252e76 fix(napplet): correct wire shapes to @napplet/nap message types
Verified the result field names and wire discriminants against the
canonical @napplet/nap 0.15.0 message types and value-types, and fixed
several mismatches:

- identity reads return method-specific fields, not a generic "result":
  getProfile->profile, getRelays->relays, getFollows/getMutes/getBlocked
  ->pubkeys, getList->entries, getZaps->zaps, getBadges->badges.
- getProfile now builds a ProfileData object ({name, displayName, about,
  picture, banner, nip05, lud16, website}) from parsed kind-0 metadata,
  mapping display_name -> displayName, instead of dumping raw content.
- storage wire types are storage.get/set/remove/keys (the SDK functions
  are getItem/setItem/removeItem/keys); storage.keys returns `keys`.
- relay.publish / publishEncrypted carry the unsigned template in the
  `event` field (per @napplet/shim), not `template`.

Shim updated to match; codec round-trip tests lock the shapes.

Still open and documented: the transport is structured-clone objects
(not JSON strings) with a real Blob for resource.bytes, and
relay.subscribe uses relay.event/relay.eose push — the shell relay needs
an object<->string bridge + push channel, with on-device verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 14:42:28 +00: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 e54c78a6ea docs(napplet): ecosystem-compatibility audit vs upstream SDK / demo runtimes
Audits the implementation against napplet/naps (specs), napplet/web (@napplet/shim
SDK) and kehto/web (reference runtime + playground). Finding: the security core
(process isolation, verified blobs, consent, ledger, permissions UI) is solid and
ahead of what the demo runtimes specify, but the edge layer is not wire-compatible
with the ecosystem — upstream uses a namespaced window.napplet.* and a
{type:"domain.action", id} envelope, we use a flat API and {id, payload:{op}}, and
we lack the mandatory shell.supports() plus the resource/upload/keys domains. So no
real ecosystem napplet runs as-is. Doc includes a coverage scorecard and a
prioritized path to compatibility (envelope + namespaced shim first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 16:13:12 +00:00
Claude 6432737424 i18n(napplet): localize all user-facing napplet strings
Moves every inline English literal in the napplet UI to string resources:

- Consent dialog: operation summaries (per op), capability label, button labels,
  and the title fallback now come from strings.xml. The payment amount is a proper
  <plurals> (sat/sats) via pluralStringRes.
- Permissions screen + Napplets list: capability names/descriptions, empty states,
  and the untitled-napplet fallback are localized.
- Host toasts (invalid napplet / WebView too old) localized.
- New NappletCapabilityLabels.kt maps each capability to shared label/description
  string resources, reused by both the consent dialog (getString) and the
  permissions screen (stringResource), so there's one source of truth.

Programmatic strings that cross the API boundary to the applet's JS (broker
Failed reasons) and log messages are intentionally left in English.

:amethyst:compileFdroidDebugKotlin passes; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 15:56:42 +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 075be5f8b7 feat(napplet): wallet (NWC) payment + live relay query; defer inter-applet
#1 WALLET — PayInvoice now pays via the user's connected NWC wallet
(account.sendZapPaymentRequestFor, wrapped suspend with a 60s timeout). The
gateway returns the preimage on success and throws (→ Failed) on no-wallet,
wallet error, or timeout, so an applet never wrongly believes a payment landed.
The consent dialog decodes the invoice and shows the amount in sats
(LnInvoiceUtil). Gated as before: must declare `value`/`wallet`, then consent.

#3 live relay query — QueryEvents now does a bounded live fetch
(INostrClient.fetchAll, EOSE/8s timeout) across the user's read relays, merged
with LocalCache, deduped, newest-first, limit-respected — instead of cache-only.

#2 inter-applet — deferred per design review. It needs new architecture
(multi-applet hosting + an archetype registry), not just a gateway, and would
risk forking the upstream NAP-INC/INTENT wire format. Surveyed upstream
napplet/naps and wrote the design + prerequisites in
amethyst/plans/2026-06-20-napplet-inter-applet.md.

:amethyst:compileFdroidDebugKotlin passes; spotless clean. Wallet + live query
need on-device verification (real NWC wallet / relays).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 14:40:46 +00:00
Claude 86a19012b5 test(napplet): JVM unit tests for the protocol codec; move it off org.json
The codec is the one place the trust boundary parses untrusted applet input, so
it deserves tests — but Android stubs `org.json` in JVM unit tests
(returnDefaultValues), making the previous org.json-based codec untestable
off-device. Rewrites NappletProtocolJson on kotlinx.serialization (a real JVM
JSON impl the app already uses for @Serializable routes), which also drops a
runtime Android dependency from the parser.

Adds NappletProtocolJsonTest (20 cases): decode of every request op, unknown op
→ null, malformed/missing-field → throws (caught by the broker as Failed), encode
of every response variant, null→JsonNull for storage/paid, and a SignedEvent
round-trip back through Event.fromJson.

:amethyst:testPlayDebugUnitTest passes (20/0/0); spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 14:13:11 +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 2bccab797c feat(napplet): relay subscription to discover napplet manifests
NappletsScreen previously showed only what was already cached. Adds a lightweight
discovery subscription so the list actually populates:

- NappletsFilterAssembler / NappletsFilterSubAssembler (SingleSubEoseManager):
  one REQ per read relay for kinds 15129/35129, deduped to a single subscription
  per account. No follow-list/feed-state machinery — the screen reads LocalCache
  directly, so the query state carries only the account + scope.
- Registered as an app-lifetime singleton in RelaySubscriptionsCoordinator (the
  EOSE manager opens its relay sub at construction, so it can't be per-screen).
- NappletsScreen invokes NappletsFilterAssemblerSubscription, which subscribes
  on STARTED and tears down after the lifecycle grace window.

:amethyst:compileFdroidDebugKotlin passes; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 03:26:15 +00:00
Claude d4899c2afc feat(napplet): UI entry point, Tor-routed blob fetch, sandbox process isolation
UI entry point:
- NappletsScreen: a "Napplets" drawer item lists napplet manifests in the local
  cache (NIP-5D kinds 15129/35129) and opens the selected one in the sandboxed
  host. Wired as Route.Napplets with a NavBarItem + drawer entry.

Sandbox process isolation (security fix):
- Application.onCreate runs in every process, so the :napplet process was building
  AppModules and initiate() was loading the account + constructing the signer there
  — defeating the "no keys in the sandbox" guarantee. Amethyst.onCreate now detects
  the :napplet process and skips AppModules entirely, leaving `instance` unset so
  any accidental use fails fast.

Tor/proxy-aware blob fetch:
- The host's OkHttpClient now routes Blossom blob fetches through the user's Tor
  SOCKS proxy when active. The port is resolved in the main process by the launcher
  and passed via the Intent, so the sandbox process never needs the account-bound
  HTTP stack.

:amethyst:compileFdroidDebugKotlin passes; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 03:10:12 +00:00
Claude daae12b53d feat(napplet): Android sandbox host — isolated process, broker IPC, consent
Implements the Android side of the napplet/nsite trust boundary on top of the
commons core. The applet runs in a separate OS process holding no keys; every
dangerous operation is brokered to the main process and gated by user consent.

:napplet process (no secrets):
- NappletHostActivity: hardened WebView (no file/content access, no DOM storage,
  mixed-content blocked, SafeBrowsing on), applet served into an opaque-origin
  sandboxed iframe, manifest blobs served already-verified via
  shouldInterceptRequest with a default-deny CSP (connect-src 'none' = no direct
  network), and a window.napplet.* shim bridged over an origin-restricted
  WebMessageListener.

Main process (holds the signer):
- NappletBrokerService: bound Messenger service running the commons NappletBroker
  against the live account; exported=false + UID check. Builds the broker per
  request so account switches are honored; relay publish via the account's
  computed broadcast relays.
- NappletConsentActivity + NappletConsentCoordinator: capability-consent dialog
  with a suspend bridge; fails closed on dismissal.
- DataStoreNappletPermissionStore: persistent grant store.

Shared edge: NappletProtocolJson (JSON codec), NappletIpc (Messenger contract),
NappletLauncher (packs a verified manifest into the host Intent), shell.html.

Adds androidx.webkit (Apache-2.0) for the origin-restricted message bridge — a
plain @JavascriptInterface leaks into every frame and would break the boundary.
Manifest declares the :napplet activity, the consent activity, and the broker
service. :amethyst:compileFdroidDebugKotlin passes; on-device verification and a
UI entry point are the remaining steps (see the plan doc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-19 23:50:58 +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
Vitor PamplonaandGitHub 19b1797f2d Merge pull request #3298 from vitorpamplona/claude/blossom-refactor-consolidate-9z9yxw
Centralize Blossom URL and auth header construction
2026-06-19 18:49:18 -04:00
Vitor PamplonaandGitHub ba6196d542 Merge pull request #3297 from vitorpamplona/claude/disappearing-scaffold-overshoot-ew8y84
Fix disappearing bar overshooting on fast reveal fling
2026-06-19 18:49:11 -04: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 f089ef9db4 fix: stop DisappearingScaffold bars overshooting their resting edge
The settle/reset animation drove a critically-damped spring with the
fling's leftover velocity. A critically-damped spring does not oscillate
from rest, but when handed an initial velocity in the target's direction
its response still crosses the target once before decaying back. On a fast
reveal fling the top bar's offset shot well past 0 (measured ~+190px in a
test) — rendering the bar sliding below its resting position and springing
back, the "goes beyond its final position and then comes back" wobble that
only appeared on fast flings.

Clamp the settle Animatable to the visible travel range [-limit, 0] via
updateBounds, so hitting an edge ends the animation crisply with no
rebound. Add a regression test that steps the settle under a manual frame
clock and asserts the offset never crosses the resting edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEGCrB5uRBAZES1PSp4Ctz
2026-06-19 22:38:20 +00:00
Vitor PamplonaandGitHub 977c94e4c6 Merge pull request #3296 from vitorpamplona/claude/napplet-protocol-n2ihd6
Add NIP-5A static-site resolver + NIP-5D napplet support
2026-06-19 18:26:36 -04:00
Claude 0eea00543a feat(cli): add amy napplet fetch for NIP-5D napplets
Extends the CLI to fetch and verify NIP-5D napplet kinds, mirroring `amy nsite`
but adding the napplet-specific runtime checks.

- NappletCommands: `amy napplet fetch AUTHOR [--d ID] | --snapshot EVENT-ID
  [--path P] [--server …] [--relay …] [--out FILE] [--timeout SECS]`. Fetches a
  root (15129), named (35129, via --d), or snapshot (5129, via --snapshot
  <event-id>) manifest; recomputes the NIP-5A aggregate hash and refuses a
  manifest whose `x` tag doesn't match its path tags (`aggregate_mismatch`)
  before touching any blob; then resolves the path with per-blob sha256
  verification. Output adds `requires` (NAP capabilities), `aggregate_sha256`,
  and `aggregate_verified`.
- StaticSiteFetch: new shared helper holding the Blossom download + resolve +
  emit logic, so `nsite` and `napplet` don't duplicate it. NsiteCommands is
  slimmed down to use it (also now reports the manifest `kind`).

Smoke-tested offline: bad-args, help, and dead-relay runs resolving cleanly to
not_found with the correct kind for all three napplet variants (15129/35129/5129)
plus a no-regression check on `nsite fetch`. The aggregate/per-blob verification
logic itself is covered by the quartz unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 22:14:14 +00:00
Claude 2648771429 feat(quartz): implement NIP-5D (Nostr Web Applets / napplets)
Adds the full NIP-5D napplet manifest layer, plus the NIP-5A aggregate-hash
infrastructure it depends on. Follows the nip88Polls package structure (event
class + tags/ + TagArrayExt + TagArrayBuilderExt).

NIP-5A shared infra (nip5aStaticWebsites):
- XTag — the aggregate-hash tag ["x", "<sha256>", "aggregate"].
- SiteAggregateHash — computes/verifies the NIP-5A aggregate hash: sort the
  per-path lines "<hash> <path>\n" lexicographically, concat as UTF-8, SHA-256.
  Pinned by a test against an independently computed sha256sum vector.
- siteAggregateHash() parse + builder extensions.

NIP-5D napplets (nip5dNapplets):
- NappletSnapshotEvent (5129, regular), RootNappletEvent (15129, replaceable),
  NamedNappletEvent (35129, addressable, d-tag) — all built on the NIP-5A
  path/server/title/description/source/x tag set.
- RequiresTag — ["requires", "<bare-nap-name>"] capability declarations.
- NappletManifest interface — uniform accessors (paths/servers/requires/title/
  …) plus computeAggregateHash()/verifyAggregate() shared across the three kinds.
  build() auto-stamps the x aggregate (required for snapshots, recommended for
  root/named).
- Registered all three kinds in EventFactory.

This covers the NIP-5D runtime verification contract end-to-end in quartz:
signature (core Event.verify), per-blob sha256 (StaticSiteResolver.verify), and
the aggregate x-tag (NappletManifest.verifyAggregate). Note the napplet kinds
5129/15129/35129 are distinct from the NIP-5A nsite kinds 5128/15128/35128, so
there is no collision.

Tests: SiteAggregateHashTest (vector + order-independence + tamper) and
NappletEventTest (build/parse round-trip for all three kinds, aggregate
verification, tamper detection, EventFactory routing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 22:06:49 +00:00
Vitor PamplonaandGitHub 80aa42a599 Merge pull request #3295 from vitorpamplona/claude/lucid-planck-0skkio
Fix metadata bitmap recycling crash on some OEM ROMs
2026-06-19 17:59:46 -04:00
Vitor PamplonaandGitHub b4b21d2aec Merge pull request #3294 from vitorpamplona/claude/brave-sagan-0vp8wh
Improve error message for failed media downloads
2026-06-19 17:48:16 -04:00
Vitor PamplonaandGitHub 575f17fac8 Merge pull request #3292 from vitorpamplona/claude/elegant-goldberg-qzkt7c
Swallow signer exceptions in relay auth and bunker responses
2026-06-19 17:43:29 -04:00
Vitor PamplonaandGitHub 69c918e086 Merge pull request #3291 from vitorpamplona/claude/zealous-gauss-6njlj1
Deduplicate emoji pack entries by shortcode and visibility
2026-06-19 17:42:53 -04:00
Vitor PamplonaandGitHub 1c85064c3f Merge pull request #3290 from vitorpamplona/claude/charming-ritchie-62gfxz
Handle missing camera app gracefully in photo/video capture
2026-06-19 17:40:04 -04:00
Vitor PamplonaandGitHub 9520ee3e5e Merge pull request #3289 from vitorpamplona/claude/busy-planck-pmnfni
Refactor feed sorting to use extension function and fix race condition
2026-06-19 17:38:38 -04:00
Claude 10caf09d47 fix: add descriptive message to media download HTTP check
When saving media to the gallery, a non-2xx response triggered a bare
check(response.isSuccessful), which threw IllegalStateException("Check
failed.") with no context. The error was caught and logged, but the
message was useless for diagnosing failures and produced a generic toast.

Include the URL, HTTP status code, and status message in the check so the
log and downstream error handling explain why the download failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cwv4DpPLTyiJP2Thv3H3jR
2026-06-19 21:37:04 +00:00
Claude 1371ba5d77 fix: dedup emoji shortcodes in EmojiPackScreen to prevent LazyGrid crash
An emoji pack can carry duplicate shortcodes: NIP-30 puts no uniqueness
constraint on emoji tags, so foreign packs may repeat them and our own
addEmoji appends without a duplicate guard. The grid keyed items on
"${code}-${priv|pub}", so two same-code entries in the same visibility
bucket produced identical keys (e.g. "kohakucho-pub") and crashed the
LazyVerticalGrid with IllegalArgumentException.

Collapse to one cell per (shortcode, visibility) with distinctBy when
building the list. Beyond fixing the crash this is the correct UX: two
cells with the same shortcode are indistinguishable and share one delete
path (removeEmoji deletes by shortcode, dropping both). Dedup on code +
visibility so a legit public/private pair of the same shortcode survives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01346aiAXBbdg5hMTAGydTqp
2026-06-19 21:31:59 +00:00
Claude 42187bbf28 fix(playback): cap media-session artwork to platform bitmap limit
Some OEM ROMs (e.g. LineageOS/peridot on Android 15) crash with
"cannot use a recycled source in createBitmap" inside
MediaMetadata.Builder.scaleBitmap() when the legacy MediaSession path
sets metadata artwork.

media3 size-limits artwork using Resources.getSystem()'s
config_mediaMetadataBitmapMaxSize, which is unresolvable on these ROMs
and falls back to the full screen width. The over-sized bitmap is then
re-scaled by android.media.session.MediaSession.setMetadata(), and those
ROMs recycle the source bitmap during scaling. media3's
CacheBitmapLoader caches the now-recycled bitmap and reuses it on the
next metadata update, hitting createBitmap() on a recycled source.

Cap decoded artwork via DataSourceBitmapLoader.setMaximumOutputDimension
to the same framework limit the platform compares against (resolved from
the app context, 320dp default), so build() never re-scales the bitmap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2raH4U7FCpK99YMQfGVQS
2026-06-19 21:27:36 +00:00
Vitor PamplonaandGitHub 96b61cc54d Merge pull request #3288 from vitorpamplona/claude/determined-ritchie-g281jh
Fix unstable sort in live activity feeds by snapshotting status order
2026-06-19 17:27:08 -04:00
Claude c8525fa833 fix: handle missing camera app when taking photos/videos
ActivityResultLauncher.launch() throws ActivityNotFoundException on
devices without an app that handles IMAGE_CAPTURE / video capture
intents, crashing the app from a background dispatcher. Wrap the launch
in launchOrToast(), which runs on the main thread, catches the
exception, shows a toast, and dismisses the capture flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VhTdM5SuUo6WMTCsZz6XWG
2026-06-19 21:26:21 +00:00
Claude 889b620b44 fix(quartz): never let signer timeouts crash from auto-signing launches
An ignored external NIP-55 signer prompt surfaces as
SignerExceptions.TimedOutException. Relay auth (NIP-42) signs replies in a
fire-and-forget scope.launch whose host scope (e.g. viewModelScope) carries no
CoroutineExceptionHandler, so an uncaught timeout there reached the platform
default handler and crashed the app ("Could not sign: User didn't accept or
reject in time.").

Guard the launch in RelayAuthenticator so signing failures are swallowed and
logged (re-throwing only CancellationException). Apply the same guard to
NostrSignerRemote's incoming-bunker-response launch, which decrypts untrusted
relay data on a handler-less scope. Add RelayAuthenticatorTimeoutTest covering
the swallowed-timeout and happy-path-still-sends-AUTH cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RM8zAKJNE8aAQL5nUboso
2026-06-19 21:21:28 +00:00
Vitor PamplonaandGitHub 5d32d49540 Merge pull request #3287 from vitorpamplona/claude/practical-davinci-23zall
Deduplicate emoji packs in selection event
2026-06-19 17:14:45 -04:00
Vitor PamplonaandGitHub 7ed03ae392 Merge pull request #3286 from vitorpamplona/claude/loving-brown-671lbf
Fix ForegroundServiceDidNotStartInTimeException in NotificationRelayService
2026-06-19 17:13:22 -04:00
Vitor PamplonaandGitHub fa9acd408f Merge pull request #3285 from vitorpamplona/claude/brave-mendel-3ehjpy
Handle Health Connect binding failures gracefully
2026-06-19 17:12:11 -04:00
Claude fa7bb365c4 fix: snapshot live-stream status order before sorting to avoid TimSort contract violation
LiveStreamsFeedFilter.sort and DiscoverLiveFeedFilter.sort computed the primary
sort key, convertStatusToOrder(it.event), lazily inside the comparator. That key
reads OnlineChecker.isCachedAndOffline(url), which depends on a moving
five-minute window and on the checkOnlineCache LruCache. A background online
check can mutate that cache while the sort is running, so the same note could
compare as LIVE (order 2) in one pairwise comparison and offline (order 0) in
another. The resulting unstable ordering makes TimSort throw
"Comparison method violates its general contract!".

Snapshot the status order once per item before sorting (matching how
participantCounts/allParticipants are already precomputed) so the comparator
reads stable values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwZzCdNMQoRWbgtZrp4SKH
2026-06-19 21:11:10 +00:00
Claude 676b51d35d fix(notif): call startForeground on every start command
The ForegroundServiceDidNotStartInTimeException came from MainActivity.onResume
calling NotificationRelayService.start() on every resume. Each
startForegroundService() re-arms Android's "must call startForeground() within
the timeout" requirement, even when the service is already running and already
foregrounded. The old initializeForeground() early-returned via an
`if (foregroundStarted) return` guard once it had been started once, so later
startForegroundService() calls were never matched by a startForeground() — the
re-armed requirement went unsatisfied and the OS crashed the whole app.

ensureForeground() now runs on every onStartCommand (startForeground() is
idempotent — it just refreshes the existing notification) and rebuilds the
notification with the current relay count so repeated calls don't flicker back
to "connecting". It also stopSelf()s on every promotion-failure path (not only
ForegroundServiceStartNotAllowedException), which clears the OS fgRequired flag
and cancels the pending timeout when promotion genuinely can't happen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hCJDhJmCNkzqb7SaMWQgB
2026-06-19 21:10:27 +00:00
Claude d606b05d24 fix: avoid TimSort contract violation in feed filters
DiscoverLongFormFeedFilter.sort (and ~44 other feed filters/view models)
sorted notes with the live DefaultFeedOrder comparator, which reads
Note.createdAt() on every comparison. When another thread swaps a Note's
event mid-sort (e.g. a newer replaceable/addressable event arriving from
a relay), createdAt() changes between comparisons and TimSort throws
"Comparison method violates its general contract!"
(IllegalArgumentException).

Migrate every amethyst Set<Note>/Iterable<Note> sort from
sortedWith(DefaultFeedOrder) to the existing sortedByDefaultFeedOrder()
helper, which snapshots createdAt() once per note so the comparator stays
consistent. The pinned-chatroom comparator in ChatroomListKnownFeedFilter
is given the same snapshot treatment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Sh5XNLssw9GJNxjkxZcRS
2026-06-19 21:09:27 +00:00
Claude ba6b01a715 refactor: dedupe emoji pack addresses with distinct()
Address is a data class over (kind, pubKeyHex, dTag), the same fields
toValue() encodes, so distinct() dedupes on exactly the grid key without
the redundant toValue() projection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fxWguG7dXgwwe2Et3JBjR
2026-06-19 21:09:18 +00:00
Claude f529924f77 fix: guard Health Connect permission check against service-bind failures
Some OEM builds (e.g. ITEL S665L) report the Health Connect provider as
SDK_AVAILABLE yet fail to bind to the underlying service, so
getGrantedPermissions() throws RemoteException("Binding to service failed").
hasAllPermissions() ran this call without any error handling, and since it is
launched from a LifecycleResumeEffect coroutine the exception propagated
uncaught and crashed the app.

Catch the failure (matching the existing pattern in readNewWorkouts/aggregate)
and treat it as "not granted" so the workout carousel quietly stays in its
prompt state instead of crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmuJDzzin15Nfsn1jUYkHh
2026-06-19 21:05:37 +00:00
Claude e5cb5439ff fix: dedupe emoji pack addresses in My Emoji List grid
A kind 10030 emoji selection event can carry the same `a` tag more than
once. MyEmojiListScreen keys its LazyVerticalGrid items by
address.toValue(), so a duplicate address crashed Compose with
"Key ... was already used". Deduplicate on the same value used as the
grid key before rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fxWguG7dXgwwe2Et3JBjR
2026-06-19 21:01:17 +00:00
Claude a9439d014f fix(notif): always stopSelf when foreground promotion fails
ForegroundServiceDidNotStartInTimeException crashes the whole app when a
service started via startForegroundService() never successfully calls
startForeground() within Android's ~10s window.

NotificationRelayService.initializeForeground() only called stopSelf() on
ForegroundServiceStartNotAllowedException. Any other failure to promote to
the foreground (OEM-specific RemoteException/IllegalStateException, a
resource lookup failure while building the notification, etc.) was logged
but left the service in a "started but not foregrounded" zombie state,
guaranteeing the timeout crash.

Now stopSelf() runs on every failure path, which clears the OS's fgRequired
flag and cancels the pending timeout. onStartCommand also bails early
(START_NOT_STICKY) when foreground promotion failed, so we don't spin up
relay coroutines on a service that's tearing itself down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hCJDhJmCNkzqb7SaMWQgB
2026-06-19 20:59:27 +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 b547b07747 feat(quartz): add NIP-5A static-site / napplet resolver
Adds a platform-agnostic resolver for NIP-5A static-website / napplet (NIP-5D)
manifests in quartz commonMain, under nip5aStaticWebsites/resolver/:

- StaticSitePathLookup: request-path normalization (query/fragment stripping,
  leading-slash insensitivity, root/dir -> index.html), slash-insensitive
  path lookup over a manifest's path tags, and a web-asset Content-Type guess.
- StaticSiteResolver: hash verification, Blossom candidate-URL assembly, and a
  suspend resolve() that downloads each listed server in order and accepts the
  first blob whose recomputed sha256 matches the manifest pin. HTTP is injected
  via a BlobFetcher typealias so quartz keeps no HTTP dependency.

The trust model is the point: the signed manifest is the authority, the Blossom
server is untrusted. A server that substitutes/corrupts a blob fails
verification and is skipped -- it can withhold content but never forge it.
Tests cover normalization, lookup, MIME guessing, and the security cases
(tampered server skipped -> falls through to honest server; all-tampered ->
Unresolvable; undeclared path -> PathNotInManifest without fetching).

Also adds quartz/plans/2026-06-19-napplet-nip5a-resolver.md documenting the
design and the open event-shape alignment questions (35128 vs 35129 manifest
kind, capability declaration vs NIP-89, aggregate build hash, server ordering)
to raise with the napplet author before the nsite/napplet event shape forks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 20:08:27 +00:00
Vitor PamplonaandGitHub a99e67d8be Merge pull request #3284 from vitorpamplona/claude/adoring-pascal-tbm6za
Remove unused `updated` field from translator mappings
2026-06-19 16:05:10 -04:00
Claude 39dc64ff3c fix: stop seeding a fresh timestamp into translators.json every run
The `sinceLastTag.updated` field was set to `date -u` on every
`scripts/translators.sh --seed` run, but nothing ever reads it. The
release-time credit generator only consumes `.mappings` and
`.sinceLastTag.translators`.

Because the field changed on every run, the seed-translators CI job
produced a diff (and therefore a new Crowdin/seed PR) on every push to
main even when the translator set was unchanged. Drop the field from the
seed write and from the committed JSON so the file only changes when a
contributor actually appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSoN4DDC5F1ehwGeC33652
2026-06-19 20:02:21 +00:00
Vitor Pamplona f247f5a431 updating known mappings for translators and github 2026-06-19 15:53:53 -04:00