Skip to content

feat(paja,shell,mocks): mocked interfaces for every NAP Paja has no backend for - #413

Open
dskvr wants to merge 5 commits into
mainfrom
feat/paja-mocked-naps
Open

dskvr wants to merge 5 commits into
mainfrom
feat/paja-mocked-naps

Conversation

@dskvr

@dskvr dskvr commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Paja now hosts every NAP it has no real backend for, using @kehto/mocks. Napplets can actually call those interfaces through window.napplet.<domain>, and the Interfaces panel marks them as mocked with a single ◌ and one legend line, so the panel doesn't grow.

The newly mocked domains are blossom, capture, catalog, class, display, git, hashtree, location, mesh, motion, navigation, nzb, pow, soulseek, system, torrent, value and worker.

Screenshots

These are taken from the Playwright spec tests/e2e/paja-mocked-naps.spec.ts against a real Paja host. The images live on the pr-assets/paja-mocked-naps ref, not in this branch.

Interfaces panel: mocked interfaces get the ◌ prefix and a dashed border, and the legend sits under the toggles.

Interfaces panel with mocked markers and legend

A napplet calling mocked NAPs: the probe napplet calls torrent.capabilities(), torrent.add(magnet), torrent.jobs(), git.open(), system.naps() and catalog.get(), sees a mapped denied rejection from location.current(), and receives the mocked class.assigned push.

Probe napplet rendering mock results inside Paja

What changed

Advertising a domain alone wasn't enough. The NIP-5D prelude owns the only port, so an unknown domain showed up as an empty {}, and @napplet/nap 0.37 only ships the 26 domains Paja already implements. So this PR touches three packages:

  • @kehto/mocks (minor): adds MOCK_NAPPLET_BINDINGS, one napplet-facing table per extension mock. scripts/generate-napplet-bindings.mjs generates it from each NAP's API Surface and Wire Protocol tables at the exact ref the mock records (bindings:generate, plus bindings:check to detect drift). The binding rules:
    • Each request row becomes a method, named after the API operation that maps to it (for example location.requestPermission → location.permission).
    • Arguments are the request's wire fields, in table order.
    • A method resolves with the single result field the table lists, or with the payload when there are several.
    • Pushes go through on(event) and the named onX handlers.
    • Tests pin each table to its mock: identical route sets, pushes the mock is allowed to send, and the same spec sha.
  • @kehto/shell (minor): adds NappletNamespacePreludeOptions.bindings (NappletDomainBinding), which turns those tables into methods over the private port. Tables are validated host-side by serializeDomainBindings, which throws TypeError. They only reach exposed domains, never replace a domain in NAPPLET_PRELUDE_BUILT_IN_DOMAINS, and come out frozen and protected from napplet reassignment.
  • @kehto/paja (minor): registers the 18 seeded mocks and appends the enabled ones to the frame environment. They go through the same disabled-domain and resolveEnvironment grant policy, which also gives the runtime's extension routing its isDomainAllowed grant. Paja passes the bindings to the prelude and sends the mocked class.assigned after shell.ready. In the Interfaces panel each mocked toggle gets ◌, data-mocked="true", a title note, and the legend line. Mocked domains are on by default and toggle or --capability like any other interface. New exports: PAJA_MOCKED_DOMAINS, PAJA_MOCKED_MARK, isPajaMockedDomain, PajaMockedDomain.

Docs are updated in the package READMEs, docs/packages/{mocks,shell,paja}.md, the Paja getting-started how-to, and both mock skills. Changesets are included for all three packages.

NAP refs checked

The binding tables come from the same draft heads the mocks are pinned to (all open napplet/naps PRs):

NAP PR Head
BLOSSOM #71 5e74daa4
CAPTURE #94 438b3761
CATALOG #95 1020ae53
CLASS #16 48282798
DISPLAY #97 2c2e1b5c
GIT #137 a73bf8de
HASHTREE #73 67997706
LOCATION #125 24d2039c
MESH #122 bf9a4722
MOTION #124 3069a945
NAVIGATION #126 9a4b179e
NZB #136 3a426fe4
POW #39 d85b4252
SOULSEEK #135 892ed8d4
SYSTEM #64 43a112db
TORRENT #72 bc99787e
VALUE #30 e468764d
WORKER #107 0a54af9a

bindings:check against those refs reports the committed file as current.

The wire envelopes are conformant. The JavaScript shape is a spec-gap decision, documented in docs/packages/shell.md. NIP-5D leaves each domain's API to its NAP, and these drafts pin the wire table, so Kehto binds what the table expresses. That has some consequences:

  • Operations whose API returns handle objects take their wire fields instead. pow.mine(jobId, template, target, options) is one example; mesh.subscribe(subId, filters, ttl) is another.
  • Local-cache rows (location.state/latest, motion.state/latest) and local helpers (blossom.parseUri, pow.formatHashRate) aren't bound. They're listed in each table's gaps.
  • Soulseek states its envelope in prose (value result), which the generator encodes as a single override.
  • The real NAP-CLASS envelope precedes every shell message. The mock can only follow shell.ready.

Verification

  • pnpm build: green
  • pnpm type-check: green
  • pnpm test:unit: 234 files, 2887 tests passing
  • pnpm docs:check: green (typedoc with --treatWarningsAsErrors)
  • pnpm --filter @kehto/mocks bindings:check: "napplet-bindings.ts is current."
  • Full Playwright suite (npx playwright test): 120 passed, 1 skipped (pre-existing), including the 3 new paja-mocked-naps specs. They cover the marker/legend, real calls with fixture data plus an error mapping plus a class push, and disabling torrent removing it from window.napplet.
  • aislop 0.12.0: 93/100, the same as main (c815e65f), with the same 7 pre-existing warnings (browser-host.ts, runtime/types.ts, cvm-nostr-transport.ts, long Paja functions). This PR adds no findings. I didn't get it to 100 because those warnings are in files this change doesn't own.

Known gaps / out of scope

  • Mock results are fixture data. A napplet that works here still needs a real host.
  • Hosts other than Paja don't get mocked domains automatically. They opt in with bindings: MOCK_NAPPLET_BINDINGS; see skills/use-nap-mocks.
  • Mesh subscription refusals arrive as mesh.subscribe.result, which isn't routed to on(). The NAP folds them into the subscription's closed delivery, and the mock never refuses.
  • Firefox/WebKit weren't run; the e2e suite is Chromium only.

🤖 Generated with Claude Code

dskvr and others added 5 commits November 9, 2026 12:02
Derive one binding per extension mock from its NAP API Surface and Wire
Protocol tables at the exact ref the mock records, so a host can expose
mocked domains to napplets. Drift tests pin the table to the mock routes,
push capabilities, and spec refs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-in binding

NappletNamespacePreludeOptions.bindings lets a host expose a draft NAP (for
example one served by @kehto/mocks) as callable window.napplet.<domain>
methods over the private NIP-5D port, with pushes via on(event) and named
handlers. Tables are validated host-side and never replace built-in domains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paja registers deterministic mocks for blossom, capture, catalog, class,
display, git, hashtree, location, mesh, motion, navigation, nzb, pow,
soulseek, system, torrent, value and worker, adds them to each frame's
environment (through the same disable and grant policy) and exposes them to
napplets with the generated wire-table bindings. The Interfaces panel marks
each mocked interface with one symbol and a legend line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document the generated mock bindings, the shell's table-driven bindings and
Paja's mocked interfaces (with the wire-table spec-gap decision), add
changesets, cover the class assignment hook, and add a Paja Playwright spec
that calls mocked NAPs through window.napplet and checks the marker/legend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dskvr
dskvr requested review from hzrd149 and nrahulpub November 9, 2026 11:34

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant