# napplet > Full text of napplet.run: site pages, then docs. THE SMALL SOFTWARE DEPARTMENT # Find your next small obsession. Games, tools, and experiments. Pick something curious. Give it a home. 01 / PICKED FOR YOU ## Open. Play. Repeat. A few things worth spending a little time with. Play here, or take them to another shell. ![Freeze Frame Response interface](https://napplet.run/showcase/images/freeze-frame-response.webp)Signed release Physics puzzle ### Freeze Frame Response Freeze the blast. Place trampolines, funnels, and cushions. Resume time and see what happens. [Run Freeze Frame Response](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkuttzvy6nyefnxpjr2e3jqg5uk8)[Source ↗](https://blossom.napplet.soy/e747ab4f9e35a11fd11bd9cf153edd9d49a1ce20debbbe6910c7df27e2e9924e) ![Stick Brawl interface](https://napplet.run/showcase/images/stick-brawl.webp)Signed release Survival game ### Stick Brawl One green stickman. Endless waves of trouble. Punch, dodge, and climb your way through the city. [Run Stick Brawl](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutf4x56xyvej89jxywfn5x7t9w)[Source ↗](https://blossom.napplet.soy/9794d3420d4d8ef9b3800b7014abb6b742b8e102fea15d8e3787248c4dec524c) ![Poly Dogfight interface](https://napplet.run/showcase/images/poly-dogfight.webp)Signed release Flight game ### Poly Dogfight Pilot a biplane around a tiny planet. Thread the rock arches, keep your speed up, and take on a dogfight. [Run Poly Dogfight](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutf5x3nrvvm9x56ryvnzrgxw9k)[Source ↗](https://blossom.napplet.soy/fbf8fc78f64d293aa88ebd4e1afb4e9ba668c8621f0e124af4476183c37ecca1) ![Impossible Machine interface](https://napplet.run/showcase/images/impossible-machine.webp)Archived release Physics playground ### Impossible Machine Arrange ramps and bumpers. Drop a marble. Find your own way into the cup. [Run Impossible Machine](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutf5xajrjdmyvyukgwpse4j4n8)[Source ↗](https://blossom.napplet.soy/db7c8101167c4b0799f1ec4acf2dbf0552f83586a70e74d8a67745fe8294d996) ![Rail Game interface](https://napplet.run/showcase/images/rail-game.webp)Archived release Arcade game ### Rail Game Recruit a squad, dodge obstacles, and take on eight wardens. [Run Rail Game](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutfex9jkxwfsvvmxverxkmltgf)[Source ↗](https://blossom.napplet.soy/dad7b678e25f96a35fc8f55d14500aaca8e9fc90ca0284a8c38f975c48d9709a) ![Sketch Loop interface](https://napplet.run/showcase/images/sketch-loop.webp)Archived release Creative tool ### Sketch Loop Turn a few lines of code into looping art. Tweak a sketch and watch it come alive. [Run Sketch Loop](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutfexgukycejvyurscmzzlcp6e)[Source ↗](https://blossom.napplet.soy/5e0385e3c08f944f6c2023a3952c87649229fe2f5fd24c07c35d547a7897cc37) The three archived releases retain their original historical manifests. Every preview checks its publisher’s signature and original artifact hash before running. This preview supplies no shell services. 02 / OUT IN THE WORLD ## From the community. Signed releases found on public relays. These are publisher descriptions, not editorial picks or a conformance certification. Relay snapshot · 2026-11-01 · 96 releases A card shown without publisher artwork asks the configured public relays for a trusted Napshot preview as it comes into view. Scrolling past the saved collection, or checking, looks for new releases on the same relays. No account is needed. [Details ↓](https://napplet.run/apps/#discovery-details) **S** ### SeedHammer 3D viewer Publisher [Snick](https://nostr.at/npub1zj0q0eseamwn9er4drf3jl3kufc0fddsfrr0v54l2mlfqenah7ysxuy9th "149e07e619eedd32e47568d3197e36e270f4b5b048c6f652bf56fe90667dbf89") 2026-11-01 3D viewer for the Seed I controller and the Hammer and II engraving machines: section cuts, part visibility, exploded views and STEP download. - cad-viewer Needs: inc, resource · Optional: theme, link, outbox, fs, storage · 24 pinned versions Needs inc, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztndqkhv6t9wajhynge70r)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztndqkhv6t9wajhynge70r)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztndqkhv6t9wajhynge70r)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztndqkhv6t9wajhynge70r) **S** ### Squad Table Publisher [8494cb65…55fe12](https://nostr.at/npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt "8494cb6579081c56693b4668968b92f75722c19a03152841f49a85a1ff55fe12") 2026-10-31 Squad-level WWII hex wargame for two players, live or play-by-mail. The rules are enforced: locked turn phases, movement points, line of sight, fire on the Infantry Fire Table with automatic morale checks, rally and close combat. Server-rolled dice, plus hotseat play on one device. - game - wargame - board-game - multiplayer - hex No required shell services declared · Optional: cvm, webrtc, identity, storage, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943nqdpnvgcxxvrxx9jqf7wfuh) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943nqdpnvgcxxvrxx9jqf7wfuh)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943nqdpnvgcxxvrxx9jqf7wfuh)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943nqdpnvgcxxvrxx9jqf7wfuh)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943nqdpnvgcxxvrxx9jqf7wfuh) **F** ### Fotohop Publisher [sandwich](https://nostr.at/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f") 2026-10-31 Layered image editor with drawing, text, cropping, adjustments, and PNG export Needs: fs · Optional: keys, storage, theme Needs fs · open it in a shell Open in[Paja ↗](https://kehto.github.io/web/paja/?pointer=nevent1qvzqqqpmrypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqgx9sw6aw020jrllzl8rxkslwkmse0cenm8vju6ulujaa94qaadkxcpwc624)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=nevent1qvzqqqpmrypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqgx9sw6aw020jrllzl8rxkslwkmse0cenm8vju6ulujaa94qaadkxcpwc624)[Signed release ↗](https://njump.me/nevent1qvzqqqpmrypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqgx9sw6aw020jrllzl8rxkslwkmse0cenm8vju6ulujaa94qaadkxcpwc624) **S** ### Stick Runner Publisher [Nose-whistle](https://nostr.at/npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2 "993dfad40fef286b323b9217a15a144bf2a2372987c159deea665462051856c0") 2026-10-31 Help Rog the stickman parkour across city rooftops through 10 levels, each harder than the last. Jump the pits, dodge spikes, saws and a fire-breathing dragon, slide on ice and bounce on springs, then go down the stairs and beat the giant spider boss! Pick Easy, Normal or Hard, and press M to switch between 2D, first person and third person! - game - platformer - parkour - stickman - 3d No required shell services declared · Optional: config, gamepad [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unxwrxvgcxzcfhvgmsnxdwqd) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unxwrxvgcxzcfhvgmsnxdwqd)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unxwrxvgcxzcfhvgmsnxdwqd)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unxwrxvgcxzcfhvgmsnxdwqd)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unxwrxvgcxzcfhvgmsnxdwqd) **C** ### Conway's Game of Life Publisher [DeadPixel](https://nostr.at/npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc "d4c58a83fea9c6ab30aa7a86810d6f4ed76ace2b1425f3e889d2c85ea0192f1e") 2026-10-31 Conway's Game of Life on a wrapping 120 by 90 board: paint cells, drop classic patterns like the Gosper glider gun, and watch it evolve at your chosen speed. - game-of-life - cellular-automaton - simulation - sandbox Needs: storage Needs storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mrgdrrxs6k2v3jvvmsgxqu9z)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mrgdrrxs6k2v3jvvmsgxqu9z)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mrgdrrxs6k2v3jvvmsgxqu9z)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mrgdrrxs6k2v3jvvmsgxqu9z) **S** ### Stick Brawl Publisher [Nose-whistle](https://nostr.at/npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2 "993dfad40fef286b323b9217a15a144bf2a2372987c159deea665462051856c0") 2026-10-30 Fight off endless waves of colorful stickmen as the green stickman. Punch, kick, drop-kick, slide, quick-draw a revolver and swing a sword through a five-floor apartment building, a fire escape and a brick warehouse. Watch out for big Brutes and tiny Runts! - game - fighting - stickman - action No required shell services declared · Optional: config, gamepad [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956n2drzxverjerz8yesg36xn7) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956n2drzxverjerz8yesg36xn7)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956n2drzxverjerz8yesg36xn7)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956n2drzxverjerz8yesg36xn7)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956n2drzxverjerz8yesg36xn7) **F** ### Freedoom Publisher [a252ce5f…2bd441](https://nostr.at/npub15ffvuhu6yyd65g5knnaacj3gklphsp2464pwag3dqxu3xppt63qstj9qnz "a252ce5f9a211baa22969cfbdc4a28b7c3780555d542eea22d01b913042bd441") 2026-10-30 DOOM-engine shooter: doomgeneric + Freedoom Phase 1, all in one napplet No required shell services declared · Optional: storage, resource [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpgjjee0e5ggm4g3fd88mm39z3d7r0qz4t42za63z6qdezvzzh4zpqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrxwfjk2er0daksz8fnaj) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpgjjee0e5ggm4g3fd88mm39z3d7r0qz4t42za63z6qdezvzzh4zpqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrxwfjk2er0daksz8fnaj)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpgjjee0e5ggm4g3fd88mm39z3d7r0qz4t42za63z6qdezvzzh4zpqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrxwfjk2er0daksz8fnaj)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpgjjee0e5ggm4g3fd88mm39z3d7r0qz4t42za63z6qdezvzzh4zpqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrxwfjk2er0daksz8fnaj)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpgjjee0e5ggm4g3fd88mm39z3d7r0qz4t42za63z6qdezvzzh4zpqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrxwfjk2er0daksz8fnaj)[Source ↗](https://github.com/vitorpamplona/amethyst/tree/main/tools/napplet-doom) **P** ### Pokédex Publisher [Alex Gleason](https://nostr.at/npub1q3sle0kvfsehgsuexttt3ugjd8xdklxfwwkh559wxckmzddywnws6cd26p "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd") 2026-10-30 Browse all 1025 Pokémon on your desktop: artwork, types, Pokédex entries and base stats, from PokéAPI. Needs: resource, storage Needs resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmsda4k2er90q8jd08e)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmsda4k2er90q8jd08e)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmsda4k2er90q8jd08e)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmsda4k2er90q8jd08e) **W** ### WebRTC Probe Publisher [Napplet Tester](https://nostr.at/npub170t6zffercx92qktuxvnury78jp8fgwafmrj7gv6n52l9ar60x8qt52gxx "f3d7a125391e0c5502cbe1993e0c9e3c8274a1dd4ec72f219a9d15f2f47a798e") 2026-10-30 Checks napplets have no WebRTC or direct network No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrhv438yarr94c8ymmzv5vwuvfl) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrhv438yarr94c8ymmzv5vwuvfl)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrhv438yarr94c8ymmzv5vwuvfl)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrhv438yarr94c8ymmzv5vwuvfl)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrhv438yarr94c8ymmzv5vwuvfl) **N** ### Napplet Test Harness Publisher [Napplet Tester](https://nostr.at/npub170t6zffercx92qktuxvnury78jp8fgwafmrj7gv6n52l9ar60x8qt52gxx "f3d7a125391e0c5502cbe1993e0c9e3c8274a1dd4ec72f219a9d15f2f47a798e") 2026-10-30 Exercises Amethyst napplet domains and sandbox isolation Needs: identity, relay, storage, resource · Optional: theme Needs identity, relay, storage, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv9c8qmr9wskhgetnwsq500nr)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv9c8qmr9wskhgetnwsq500nr)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv9c8qmr9wskhgetnwsq500nr)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpu7h5yjnj8sv25pvhcve8cxfu0yzwjsa6nk89use48g47t6857vwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv9c8qmr9wskhgetnwsq500nr) **S** ### Shiftfield Publisher [DeadPixel](https://nostr.at/npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc "d4c58a83fea9c6ab30aa7a86810d6f4ed76ace2b1425f3e889d2c85ea0192f1e") 2026-10-29 — the Roguelike Minesweeper — Clear sectors, bank scrap, die rich. Explore deeper depths for more riches, but watch out for hazards, and the ground that shifts beneath your feet. Built 100% locally and offline with StrataLLM and Qwen3.8 Flash Next - game - puzzle - roguelike No required shell services declared · Optional: storage, config, cvm [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jngenzxcengvtrvdjq5zlyaf) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jngenzxcengvtrvdjq5zlyaf)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jngenzxcengvtrvdjq5zlyaf)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jngenzxcengvtrvdjq5zlyaf)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzp4x932pla2wx4vc2575xsyxk7nkhdt8zk9p9705gn5kgt6spjtc7qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jngenzxcengvtrvdjq5zlyaf) **M** ### Mini Samurai Publisher [craybird](https://nostr.at/npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r "57f7119a0ce9ec9cec24958dc0ade5cbd538a722e0ec4eb4804e64dc532f1aad") 2026-10-28 Tiny samurai fight in a giant house. Pick the living room, kitchen, bedroom, bathroom or garage, or the whole house with a rideable toy train. Duel 1v1, 2v2 or a four-way free for all against friends or the CPU, in split screen. Climb furniture, hang from ledges, dodge the ceiling fan and the toaster, swim in the bath, and when your katana is knocked away fight on with forks, pencils, nunchaku, marbles and water balloons. Smash the garage crates for drifting toy cars. - game - fighting - samurai - 3d - multiplayer No required shell services declared · Optional: config, storage [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ekyv3cvccx2enrx43qn49xze) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ekyv3cvccx2enrx43qn49xze)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ekyv3cvccx2enrx43qn49xze)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ekyv3cvccx2enrx43qn49xze)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ekyv3cvccx2enrx43qn49xze) **F** ### Follow Pack Creator Publisher [sandwich](https://nostr.at/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f") 2026-10-28 Create, curate, and manage Nostr follow packs with your network and Relatr profile search. Needs: identity, outbox · Optional: common, cvm, keys, link, resource, storage, theme Needs identity, outbox · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmyv4nxzatvws8kgswq)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmyv4nxzatvws8kgswq)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmyv4nxzatvws8kgswq)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmyv4nxzatvws8kgswq) **A** ### A ping-pong game with powerups Publisher [sandwich](https://nostr.at/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f") 2026-10-28 A ping-pong game with powerups. 2 player only. split keyboard or two controllers. No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqfrpwpcxcet595mxgefcxv6xgd3evymr2d3k8p3kvc3hv5kk2dehx9skvvrzzepdp0) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqfrpwpcxcet595mxgefcxv6xgd3evymr2d3k8p3kvc3hv5kk2dehx9skvvrzzepdp0)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqfrpwpcxcet595mxgefcxv6xgd3evymr2d3k8p3kvc3hv5kk2dehx9skvvrzzepdp0)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqfrpwpcxcet595mxgefcxv6xgd3evymr2d3k8p3kvc3hv5kk2dehx9skvvrzzepdp0)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqfrpwpcxcet595mxgefcxv6xgd3evymr2d3k8p3kvc3hv5kk2dehx9skvvrzzepdp0) **F** ### Funclet Publisher [Schlaus Kwab](https://nostr.at/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf "3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3") 2026-10-28 A little window into a world of radio. Explore Wavefunc stations, listen live, read your favourite lists, and discover what is playing. - radio - music - wavefunc Needs: outbox, media · Optional: storage, identity, cvm, resource, config, link Needs outbox, media · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mxydmpxvmkxctrx9jqdgyc87)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mxydmpxvmkxctrx9jqdgyc87)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mxydmpxvmkxctrx9jqdgyc87)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95mxydmpxvmkxctrx9jqdgyc87) **O** ### Outbreak: Soycoon City Publisher [Schlaus Kwab](https://nostr.at/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf "3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3") 2026-10-28 A PS1-inspired fixed-camera survival horror demo starring Soybert, with six connected rooms, a graphical level editor, controller support and player levels shared on Nostr. - game - editor - survival-horror - threejs No required shell services declared · Optional: storage, config, fs, identity, outbox, upload, resource · 1 pinned version [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943rsdmrxq6r2de3vyus2q8jc9) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943rsdmrxq6r2de3vyus2q8jc9)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943rsdmrxq6r2de3vyus2q8jc9)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943rsdmrxq6r2de3vyus2q8jc9)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943rsdmrxq6r2de3vyus2q8jc9) **G** ### GRAV / RIFT Publisher [Schlaus Kwab](https://nostr.at/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf "3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3") 2026-10-28 Wield gravity. Stream scrap, rip machines apart, extract their shield cells, and hurl living enemies through a destructible scrapyard. - game - physics - pixel-art - arcade No required shell services declared · Optional: storage [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95uxzdenxccnjwp3v5uqttzhrf) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95uxzdenxccnjwp3v5uqttzhrf)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95uxzdenxccnjwp3v5uqttzhrf)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95uxzdenxccnjwp3v5uqttzhrf)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95uxzdenxccnjwp3v5uqttzhrf) **N** ### Napplet Machines V8 Publisher [Schlaus Kwab](https://nostr.at/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf "3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3") 2026-10-28 Build a little track. Race a little reckless. Six-car miniature racing with a 3D spline workshop, AI rivals and multiplayer rooms. - game - racing - multiplayer - track-editor No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943r2dfhxgenvvnyx3ssma2kps) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943r2dfhxgenvvnyx3ssma2kps)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943r2dfhxgenvvnyx3ssma2kps)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943r2dfhxgenvvnyx3ssma2kps)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943r2dfhxgenvvnyx3ssma2kps) **S** ### Soybert 3D Publisher [NappletSoy Creator](https://nostr.at/npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd "99d1d395c7be96c77766e4124164ce3a2443d486e146644f762d29832f7faf25") 2026-10-28 A rotating Soybert showcase and a small 3D stage. Run, jump, fire the shotgun and celebrate with keyboard or touch controls. - 3d - mascot - demo No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unqv33v5ckydnp8ymqmdy507) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unqv33v5ckydnp8ymqmdy507)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unqv33v5ckydnp8ymqmdy507)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unqv33v5ckydnp8ymqmdy507)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unqv33v5ckydnp8ymqmdy507) **R** ### Rail Game Publisher [NappletSoy Creator](https://nostr.at/npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd "99d1d395c7be96c77766e4124164ce3a2443d486e146644f762d29832f7faf25") 2026-10-28 Lead up to 250 soldiers against 13,075 enemies across eight long rail stages. Recruit, multiply, dodge barricades and defeat eight wardens in a geometric arcade runner. - game - arcade - runner No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unzetr8ycxxdnxv3nqpe08gx) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unzetr8ycxxdnxv3nqpe08gx)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unzetr8ycxxdnxv3nqpe08gx)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unzetr8ycxxdnxv3nqpe08gx)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95unzetr8ycxxdnxv3nqpe08gx) **R** ### Recipe Cards Publisher [ColdJuno\_f96b - Juno Sterling](https://nostr.at/npub1ctc6knn22a7a3xqt2ls4nphdj2psh9dffum5ry3r2434x26u4ssqhukdtw "c2f1ab4e6a577dd8980b57e15986ed92830b95a94f374192235563532b5cac20") 2026-10-28 The Recipes Are Everything - note No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrjv43kjur9943kzunywvx5jrql) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrjv43kjur9943kzunywvx5jrql)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrjv43kjur9943kzunywvx5jrql)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrjv43kjur9943kzunywvx5jrql)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrjv43kjur9943kzunywvx5jrql) **L** ### Longform Publisher [ColdJuno\_f96b - Juno Sterling](https://nostr.at/npub1ctc6knn22a7a3xqt2ls4nphdj2psh9dffum5ry3r2434x26u4ssqhukdtw "c2f1ab4e6a577dd8980b57e15986ed92830b95a94f374192235563532b5cac20") 2026-10-28 Create Blog like events on Nostr - note No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrvdahxwen0wfks6s3mwt) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrvdahxwen0wfks6s3mwt)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrvdahxwen0wfks6s3mwt)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrvdahxwen0wfks6s3mwt)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpsh34d8x54mamzvqk4lptxrwmy5rpw26jnehgxfzx4tr2v44etpqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrvdahxwen0wfks6s3mwt) ### Feed Manager Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Build custom Nostr feeds. Needs: identity, intent, outbox, relay, storage · Optional: theme Needs identity, intent, outbox, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxv4jkgttzw45kcer9wgy6m8q9)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxv4jkgttzw45kcer9wgy6m8q9)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxv4jkgttzw45kcer9wgy6m8q9)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxv4jkgttzw45kcer9wgy6m8q9)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/feed-builder) ### App Deployer Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Deploy a built NIP-5D app artifact. Needs: identity, outbox, upload · Optional: theme Needs identity, outbox, upload · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyrwv9c8qmr9wskkgetsd3hhjetjlegsl6)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyrwv9c8qmr9wskkgetsd3hhjetjlegsl6)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyrwv9c8qmr9wskkgetsd3hhjetjlegsl6)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyrwv9c8qmr9wskkgetsd3hhjetjlegsl6)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/napplet-deployer) ### Paint Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Draw and edit pictures. Needs: storage, upload · Optional: theme Needs storage, upload · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptsv95kuaqe94qgq)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptsv95kuaqe94qgq)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptsv95kuaqe94qgq)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptsv95kuaqe94qgq)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/paint) ### Emoji Pack Creator Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Create Nostr emoji packs. Needs: common, identity, inc, outbox, relay, resource, storage, upload · Optional: theme Needs common, identity, inc, outbox, relay, resource, storage, upload · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyn9d4hk56fdwpskx6edvdex2ct5daeq82sxtm)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyn9d4hk56fdwpskx6edvdex2ct5daeq82sxtm)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyn9d4hk56fdwpskx6edvdex2ct5daeq82sxtm)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyn9d4hk56fdwpskx6edvdex2ct5daeq82sxtm)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/emoji-pack-creator) ### Emoji Sets Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Manage Nostr emoji packs. Needs: identity, relay, resource, storage · Optional: theme Needs identity, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdd4skuct8v4equjaqjy)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdd4skuct8v4equjaqjy)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdd4skuct8v4equjaqjy)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdd4skuct8v4equjaqjy)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/emoji-manager) ### Emoji Browser Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Browse Nostr emoji packs. Needs: common, identity, inc, outbox, relay, resource, storage · Optional: theme Needs common, identity, inc, outbox, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdvfex7amnv4eq5yf38q)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdvfex7amnv4eq5yf38q)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdvfex7amnv4eq5yf38q)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt9d4hk56fdvfex7amnv4eq5yf38q)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/emoji-browser) ### Blobbi Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Interact with Blobbi on Nostr. Needs: identity, inc, relay, storage · Optional: theme Needs identity, inc, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnzd3hkycnff9x0ww)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnzd3hkycnff9x0ww)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnzd3hkycnff9x0ww)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnzd3hkycnff9x0ww)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/blobbi) ### Creator Signals Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Explore creator activity on Nostr. Needs: identity, inc, relay, storage · Optional: theme Needs identity, inc, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrmrwfjkzar0wgkhx6t8deskcuc3mcna4)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrmrwfjkzar0wgkhx6t8deskcuc3mcna4)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrmrwfjkzar0wgkhx6t8deskcuc3mcna4)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrmrwfjkzar0wgkhx6t8deskcuc3mcna4)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/creator-signals) ### Follow Packs Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Browse Nostr follow packs. Needs: identity, outbox, relay, resource · Optional: theme Needs identity, outbox, relay, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmxdakxcmmh94cxzcmtwzq3wv)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmxdakxcmmh94cxzcmtwzq3wv)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmxdakxcmmh94cxzcmtwzq3wv)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmxdakxcmmh94cxzcmtwzq3wv)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/follow-pack) ### People Radar Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Discover people on Nostr. Needs: identity, inc, relay, storage · Optional: theme Needs identity, inc, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrsv4hhqmr994exzerpwgs6vady)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrsv4hhqmr994exzerpwgs6vady)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrsv4hhqmr994exzerpwgs6vady)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrsv4hhqmr994exzerpwgs6vady)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/people-radar) ### Thread Triage Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Review Nostr threads. Needs: identity, inc, relay, storage · Optional: theme Needs identity, inc, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt5dpex2cty9468y6tpvajsxqp20j)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt5dpex2cty9468y6tpvajsxqp20j)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt5dpex2cty9468y6tpvajsxqp20j)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrt5dpex2cty9468y6tpvajsxqp20j)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/thread-triage) ### Workspace Journal Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Keep a workspace journal. Needs: identity, inc, storage · Optional: theme Needs identity, inc, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqythdaexkumsv93k2tt2da6hymnpdsq6r0q0)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqythdaexkumsv93k2tt2da6hymnpdsq6r0q0)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqythdaexkumsv93k2tt2da6hymnpdsq6r0q0)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqythdaexkumsv93k2tt2da6hymnpdsq6r0q0)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/workspace-journal) ### Reading Queue Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Manage a Nostr reading queue. Needs: identity, inc, relay, storage · Optional: theme Needs identity, inc, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4skg6twvukhzat9w4jsj4qg2p)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4skg6twvukhzat9w4jsj4qg2p)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4skg6twvukhzat9w4jsj4qg2p)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4skg6twvukhzat9w4jsj4qg2p)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/reading-queue) ### Security Lab Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Inspect sandbox and runtime capabilities. Needs: identity, inc, relay, resource, storage · Optional: theme Needs identity, inc, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrnv43h2unfw3uj6mrpvgrzsehu)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrnv43h2unfw3uj6mrpvgrzsehu)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrnv43h2unfw3uj6mrpvgrzsehu)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrnv43h2unfw3uj6mrpvgrzsehu)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/security-lab) ### Relay Browser Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Browse Nostr relays. Needs: identity, inc, relay · Optional: theme Needs identity, inc, relay · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4kxz7fdvfex7amnv4eqf0htue)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4kxz7fdvfex7amnv4eqf0htue)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4kxz7fdvfex7amnv4eqf0htue)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtjv4kxz7fdvfex7amnv4eqf0htue)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/relay-browser) ### Videos Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Browse Nostr videos. Needs: media, outbox, relay, resource · Optional: theme Needs media, outbox, relay, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnkd9jx2mmn9e5kt7)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnkd9jx2mmn9e5kt7)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnkd9jx2mmn9e5kt7)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnkd9jx2mmn9e5kt7)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/videos) ### Radio Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Listen to radio streams. Needs: inc, media, relay, resource, storage · Optional: theme Needs inc, media, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptjv9jxjmcy7zwe6)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptjv9jxjmcy7zwe6)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptjv9jxjmcy7zwe6)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptjv9jxjmcy7zwe6)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/radio) ### Direct Messenger Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Exchange direct messages on Nostr. - dm Needs: common, dm, intent · Optional: theme Needs common, dm, intent · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyryd9ex2cm594kk2umnv4hxwetjh9s80g)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyryd9ex2cm594kk2umnv4hxwetjh9s80g)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyryd9ex2cm594kk2umnv4hxwetjh9s80g)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqyryd9ex2cm594kk2umnv4hxwetjh9s80g)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/direct-messenger) ### Instant Messenger Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Direct messaging and presence on Nostr. Needs: common, dm, identity, intent, relay, resource, storage · Optional: theme Needs common, dm, identity, intent, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmgd9kswxtwap)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmgd9kswxtwap)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmgd9kswxtwap)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmgd9kswxtwap)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/him) ### Live Chat Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Join live chat on Nostr. Needs: identity, inc, outbox, relay, storage · Optional: theme Needs identity, inc, outbox, relay, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztvd9mx2ttrdpshgc59yuk)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztvd9mx2ttrdpshgc59yuk)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztvd9mx2ttrdpshgc59yuk)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztvd9mx2ttrdpshgc59yuk)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/live-chat) ### Stream Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Watch live streams on Nostr. Needs: inc, media, relay, resource · Optional: theme Needs inc, media, relay, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznvd9mx2um5wfjkzmgy706ug)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznvd9mx2um5wfjkzmgy706ug)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznvd9mx2um5wfjkzmgy706ug)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznvd9mx2um5wfjkzmgy706ug)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/stream) ### Profile Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 View Nostr profiles and notes. - profile Needs: identity, inc, intent, outbox, relay, resource, storage · Optional: theme Needs identity, inc, intent, outbox, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmswfhkv6tvv5hsze3p)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmswfhkv6tvv5hsze3p)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmswfhkv6tvv5hsze3p)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmswfhkv6tvv5hsze3p)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/profile) ### Note Reader Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Read Nostr notes and their replies. - note Needs: identity, inc, intent, relay, resource, storage · Optional: theme Needs identity, inc, intent, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmwda6x2ttkd9jhwetjswpv6x)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmwda6x2ttkd9jhwetjswpv6x)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmwda6x2ttkd9jhwetjswpv6x)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmwda6x2ttkd9jhwetjswpv6x)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/note) ### Compose Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Compose and publish Nostr notes. - composer Needs: cvm, identity, intent, outbox, relay, resource, storage, upload · Optional: theme Needs cvm, identity, intent, outbox, relay, resource, storage, upload · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmrdakhqmmnv5wheq7c)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmrdakhqmmnv5wheq7c)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmrdakhqmmnv5wheq7c)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmrdakhqmmnv5wheq7c)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/composer) ### Feed Publisher [b902388a…522278](https://nostr.at/npub1hypr3z4pu4t62ju3pqwl4ap3ll7jyuuur6kz2qhnhhym8ejjyfuqhshf92 "b902388aa1e557a54b91081dfaf431fffd22739c1eac2502f3bdc9b3e6522278") 2026-10-27 Read Nostr feeds. - feed Needs: identity, inc, intent, outbox, relay, resource, storage · Optional: theme Needs identity, inc, intent, outbox, relay, resource, storage · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqprxv4jkg3exe86)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqprxv4jkg3exe86)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqprxv4jkg3exe86)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwgz8z92re2h549ezzqalt6rrllayfeec84vy5p080wfk0n9ygncqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqprxv4jkg3exe86)[Source ↗](https://github.com/hyprgate/web/tree/master/napplets/feed) **D** ### Dine and Dash Publisher [Nose-whistle](https://nostr.at/npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2 "993dfad40fef286b323b9217a15a144bf2a2372987c159deea665462051856c0") 2026-10-25 Sneak around the diner, eat every plate of food, then dash out the door before a waiter catches you! Or play as the waiter and catch both dine and dashers before they escape. - game - stealth - food No required shell services declared · Optional: config, gamepad, keys [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95exvwp5x4snyep5xucs0vkdy2) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95exvwp5x4snyep5xucs0vkdy2)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95exvwp5x4snyep5xucs0vkdy2)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95exvwp5x4snyep5xucs0vkdy2)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95exvwp5x4snyep5xucs0vkdy2) **S** ### SoyCat Publisher [Cocktus 🌵](https://nostr.at/npub1j42rxzt0eg3r72ddl4f2psk7q73u2t4ksrumfk0td3rdjxtv2s8qk2u4jf "955433096fca223f29adfd52a0c2de07a3c52eb680f9b4d9eb6c46d9196c540e") 2026-10-24 💬🎶⚡️📻🐈 Needs: relay, outbox, identity, resource, storage, link, common, media · Optional: inc, intent Needs relay, outbox, identity, resource, storage, link, common, media · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnndaukxct54lnpf6)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnndaukxct54lnpf6)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnndaukxct54lnpf6)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpnndaukxct54lnpf6) **P** ### Pickle Rustlers Publisher [craybird](https://nostr.at/npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r "57f7119a0ce9ec9cec24958dc0ade5cbd538a722e0ec4eb4804e64dc532f1aad") 2026-10-24 A low-poly 3D western. Rope runaway pickles from horseback and drag them into the Giant Pickle Jar before sundown, while shotgunning the onion outlaws who steal them. Survive five days (a stampede, an onion tank, a dust storm), then peel the giant Onion King to save the West. Borrow an M1A1 Abrams, leap rocks, and drink hot sauce for a fire-breathing horse or a flamethrower tank. Endless mode unlocks after you win. - game - 3d - low-poly - western - arcade No required shell services declared · Optional: storage, config, keys, gamepad, identity, upload, outbox [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzep5x3jnwctzxv6spgrvsx) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzep5x3jnwctzxv6spgrvsx)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzep5x3jnwctzxv6spgrvsx)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzep5x3jnwctzxv6spgrvsx)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzep5x3jnwctzxv6spgrvsx) **P** ### Pursuit PIT Publisher [craybird](https://nostr.at/npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r "57f7119a0ce9ec9cec24958dc0ade5cbd538a722e0ec4eb4804e64dc532f1aad") 2026-10-24 First-person highway driving from the driver's seat, on an endless, winding interstate through countryside, forests, mountain passes, road works, towns with very silly names and big gridded cities. Pursuit: chase down a fleeing sedan and PIT it (clip its rear quarter to spin it out at up to 160 mph), then watch the replay. Getaway: you're the one running, from eight police cruisers. Free drive: just you, the traffic and the odd flock of birds. Drive by day, dusk or night, customise your cars in the garage, and listen to the police radio or the car stereo. Low-poly 3D with synthesized sound; keyboard, touch or a standard gamepad. - game - driving - racing - 3d - police No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95un2vf5xy6x2dmxvsmsk0jkzp) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95un2vf5xy6x2dmxvsmsk0jkzp)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95un2vf5xy6x2dmxvsmsk0jkzp)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95un2vf5xy6x2dmxvsmsk0jkzp)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95un2vf5xy6x2dmxvsmsk0jkzp) **B** ### Black Site Publisher [craybird](https://nostr.at/npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r "57f7119a0ce9ec9cec24958dc0ade5cbd538a722e0ec4eb4804e64dc532f1aad") 2026-10-24 Top-down tactical extraction shooter. See only what is in your field of view, loot supply caches, fight through five maps, call in the helicopter and survive until it lands. Includes a campaign, four characters, an armory and three difficulties. - game - shooter - tactical - top-down - extraction No required shell services declared · Optional: keys, storage, identity, common, resource, outbox [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jryv3jx4jxgcen8ycswe8cmy) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jryv3jx4jxgcen8ycswe8cmy)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jryv3jx4jxgcen8ycswe8cmy)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jryv3jx4jxgcen8ycswe8cmy)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq4lhzxdqe60vnnkzf9vdczk7tj748znj9c8vf66gqnnym3fj7x4dqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jryv3jx4jxgcen8ycswe8cmy) **P** ### Poly Dogfight Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-23 Fly a low-poly biplane around a tiny planet you can circle in about a minute. Weave through rock arches, needle spires, floating islands and the Great Rift canyon with momentum-based flight: keep your speed up or the wings stall. Up to four pilots share the screen with game controllers for split-screen dogfights: fixed guns with light aim assist, per-part battle damage, and two heat-seeking rockets on the wings that reload over time. Rockets turn slower than you do, so a hard bank can shake one, and a missed rocket coasts round the planet and may come back for its own pilot. Beginner steering banks and pulls for you; normal mode gives full manual control. Plays with keyboard, gamepad or touch, in any frame from a small widget to full screen. - game - flight - arcade - dogfight - multiplayer No required shell services declared · Optional: storage, config, gamepad, keys · 2 pinned versions [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956rge3kxdjn2dpjxf3qvz60cz) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956rge3kxdjn2dpjxf3qvz60cz)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956rge3kxdjn2dpjxf3qvz60cz)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956rge3kxdjn2dpjxf3qvz60cz)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956rge3kxdjn2dpjxf3qvz60cz) **D** ### Doodle Duo Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-23 Two phones, one doodler: co-op Doodle Jump over the mesh, one button each Needs: mesh · Optional: outbox, upload, intent, identity, theme Needs mesh · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztydahkgmr9v36k72an9at)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztydahkgmr9v36k72an9at)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztydahkgmr9v36k72an9at)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztydahkgmr9v36k72an9at) **N** ### Nes emulator Publisher [Cocktus 🌵](https://nostr.at/npub1j42rxzt0eg3r72ddl4f2psk7q73u2t4ksrumfk0td3rdjxtv2s8qk2u4jf "955433096fca223f29adfd52a0c2de07a3c52eb680f9b4d9eb6c46d9196c540e") 2026-10-23 For some fun. Needs: relay, outbox, identity, resource, storage, upload, fs Needs relay, outbox, identity, resource, storage, upload, fs · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv4ej6etdw4kxzar0wgk5jnfg)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv4ej6etdw4kxzar0wgk5jnfg)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv4ej6etdw4kxzar0wgk5jnfg)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzp925xvyklj3z8u56ml2j5rpduparc5htdq8eknv7kmzxmyvkc4qwqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrwv4ej6etdw4kxzar0wgk5jnfg) **S** ### stepstr Publisher [Snick](https://nostr.at/npub1zj0q0eseamwn9er4drf3jl3kufc0fddsfrr0v54l2mlfqenah7ysxuy9th "149e07e619eedd32e47568d3197e36e270f4b5b048c6f652bf56fe90667dbf89") 2026-10-22 Opens any STEP file in the browser, with section cuts, part visibility, exploded views, exact measurements and share links. - 3d-model Needs: inc, resource · Optional: theme, link, outbox, fs, storage · 2 pinned versions Needs inc, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmnw3jhqum5wg6jtf23)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmnw3jhqum5wg6jtf23)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmnw3jhqum5wg6jtf23)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq9y7qlnpnmkaxtj826xnr9lrdcns7j6mqjxx7eft74h7jpn8m0ufqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmnw3jhqum5wg6jtf23) **O** ### Obsidian Breaker Publisher [earthly.city](https://nostr.at/npub1dmmh0futyszl4445mv4xrygkgsgj8vpu79yx3qyy9qxmz4pemtzqlqmc87 "6ef777a78b2405fad6b4db2a619116441123b03cf148688084280db15439dac4") 2026-10-21 Shatter seven double-wide volatile mineral seams, each with up to twice the stones and a 1.5x faster ball, in a molten breakout run. Chain-explode jade crystals, bank shots off indestructible obsidian ribs, steer a live bounce-prediction seer, slow time with a rechargeable bullet-time meter, and grab blast-radius and ball-splitting powerups. - breakout - arcade - game - physics - bullet-time No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jx2dtyvvmxzvpjv5mscwtxxa) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jx2dtyvvmxzvpjv5mscwtxxa)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jx2dtyvvmxzvpjv5mscwtxxa)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jx2dtyvvmxzvpjv5mscwtxxa)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jx2dtyvvmxzvpjv5mscwtxxa) **D** ### Dodgy Duck Publisher [sandwich](https://nostr.at/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f") 2026-10-21 An endless voxel crossing adventure. Hop through changing worlds, collect travelers, and chase your best run. - game No required shell services declared · Optional: intent [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztsw4jxgmr9dphhq50x76m) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztsw4jxgmr9dphhq50x76m)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztsw4jxgmr9dphhq50x76m)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztsw4jxgmr9dphhq50x76m)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztsw4jxgmr9dphhq50x76m)[Source ↗](https://github.com/sandwichfarm/down-to-duck) **N** ### Notepad Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 A Windows XP Notepad for the shell's virtual filesystem. - text-editor Needs: fs · Optional: config, intent, storage Needs fs · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmwda6x2urpvs0p3zm2)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmwda6x2urpvs0p3zm2)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmwda6x2urpvs0p3zm2)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmwda6x2urpvs0p3zm2) **R** ### Rubik Cube Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 A 3D Rubik's Cube to orbit, twist, scramble and solve. No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznjw43xj6edvd6kyeg23fchl) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznjw43xj6edvd6kyeg23fchl)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznjw43xj6edvd6kyeg23fchl)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznjw43xj6edvd6kyeg23fchl)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznjw43xj6edvd6kyeg23fchl) **X** ### XP Theme Showcase Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 Every component, skin and token in @napplelets/theme-xp, wired to a live NAP-THEME payload. Needs: theme · Optional: config, resource, storage Needs theme · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmcwqkhx6r0wa3kzum90rh3nj)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmcwqkhx6r0wa3kzum90rh3nj)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmcwqkhx6r0wa3kzum90rh3nj)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmcwqkhx6r0wa3kzum90rh3nj) **N** ### Napplet Feed Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 Napplet manifests published by your contacts, with quick naddr copy. Needs: identity, outbox · Optional: config, theme Needs identity, outbox · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrwv9cz6en9v4jqwc3myg)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrwv9cz6en9v4jqwc3myg)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrwv9cz6en9v4jqwc3myg)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrwv9cz6en9v4jqwc3myg) **G** ### Good Morning Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 A GM inbox: good-morning notes from the people you follow, marking the ones you have greeted back. Needs: identity, outbox · Optional: intent, link, resource, theme Needs identity, outbox · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8dahkgttddaexu6twvugch559)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8dahkgttddaexu6twvugch559)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8dahkgttddaexu6twvugch559)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8dahkgttddaexu6twvugch559) **F** ### File Browser Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 Browse, preview, create, rename, move and delete files in the shell's virtual filesystem. Needs: fs · Optional: theme Needs fs · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxd9kx2ttzwfhhwum9wgp353ys)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxd9kx2ttzwfhhwum9wgp353ys)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxd9kx2ttzwfhhwum9wgp353ys)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrrxd9kx2ttzwfhhwum9wgp353ys) **M** ### Minesweeper Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 A Windows XP-style Minesweeper game with a Nostr friends feed. Needs: identity, outbox · Optional: common, config Needs identity, outbox · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmdd9hx2umhv4jhqetjwqnhfl)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmdd9hx2umhv4jhqetjwqnhfl)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmdd9hx2umhv4jhqetjwqnhfl)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzmdd9hx2umhv4jhqetjwqnhfl) **N** ### Nostr Bubbles Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 Live Nostr activity from your contacts or popular relays as bouncing profile-picture bubbles. Needs: identity, outbox · Optional: common, config, relay, resource, theme Needs identity, outbox · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtwdaehgu3dvf6kycnvv4esd8dsyy)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtwdaehgu3dvf6kycnvv4esd8dsyy)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtwdaehgu3dvf6kycnvv4esd8dsyy)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtwdaehgu3dvf6kycnvv4esd8dsyy) **H** ### Hashtree Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-21 Browse Blossom hashtree folders (BUD-15/16/17/18) read-only. Needs: resource · Optional: config, fs, identity, link, media, outbox, storage, theme Needs resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrgv9eksarjv4js5zne9z)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrgv9eksarjv4js5zne9z)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrgv9eksarjv4js5zne9z)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrgv9eksarjv4js5zne9z) **F** ### Freeze Frame Response Publisher [Schlaus Kwab](https://nostr.at/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf "3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3") 2026-10-20 A municipal physics puzzle: freeze the blast, place trampolines, funnels and sticky cushions, then resume time and file the damage report. - physics - puzzle - city - game - municipal No required shell services declared · Optional: storage, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzdfjv5enqep4vceqd3wndg) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzdfjv5enqep4vceqd3wndg)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzdfjv5enqep4vceqd3wndg)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzdfjv5enqep4vceqd3wndg)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xzdfjv5enqep4vceqd3wndg) **B** ### blockfall Publisher [earthly.city](https://nostr.at/npub1dmmh0futyszl4445mv4xrygkgsgj8vpu79yx3qyy9qxmz4pemtzqlqmc87 "6ef777a78b2405fad6b4db2a619116441123b03cf148688084280db15439dac4") 2026-10-19 Steer a thrusting rocket into stacked block towers and bring the skyline down. - game - physics - destruction No required shell services declared [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94skvctxv9jn2dt9vccsvwqpul) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94skvctxv9jn2dt9vccsvwqpul)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94skvctxv9jn2dt9vccsvwqpul)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94skvctxv9jn2dt9vccsvwqpul)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94skvctxv9jn2dt9vccsvwqpul) **T** ### Truffle Rush Publisher [earthly.city](https://nostr.at/npub1dmmh0futyszl4445mv4xrygkgsgj8vpu79yx3qyy9qxmz4pemtzqlqmc87 "6ef777a78b2405fad6b4db2a619116441123b03cf148688084280db15439dac4") 2026-10-18 Catch falling truffles, dodge crows and fill the belly meter to unleash a golden frenzy in this moonlit piggy arcade. - game - arcade - pigs - truffles No required shell services declared · Optional: storage, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jxzepsxc6rgv3jx33q3nc3kx) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jxzepsxc6rgv3jx33q3nc3kx)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jxzepsxc6rgv3jx33q3nc3kx)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jxzepsxc6rgv3jx33q3nc3kx)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqmhhw7nckfq9ltttfke2vxg3v3q3ywcreu2gdzqgg2qdk92rnkkyqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jxzepsxc6rgv3jx33q3nc3kx) **H** ### Hang On, Fren Publisher [TheCryptoDonkey](https://nostr.at/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2 "da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd") 2026-10-16 A Riviera Vespa road tribute starring DNI. Grab roses, cross ten regions, and reach the finish line before time runs out. - game - arcade - racing Needs: resource Needs resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpkse78xnf0k2gjl8fkjtxpke68wcdd358nhefn3zcjwx7kvpdedaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jrvcesxvcrscfjvyuq0kxpx5)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpkse78xnf0k2gjl8fkjtxpke68wcdd358nhefn3zcjwx7kvpdedaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jrvcesxvcrscfjvyuq0kxpx5)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpkse78xnf0k2gjl8fkjtxpke68wcdd358nhefn3zcjwx7kvpdedaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jrvcesxvcrscfjvyuq0kxpx5)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpkse78xnf0k2gjl8fkjtxpke68wcdd358nhefn3zcjwx7kvpdedaqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw94jrvcesxvcrscfjvyuq0kxpx5) **B** ### Black Site Map Editor Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-15 Create, edit and remix maps for Black Site. Draw terrain, buildings, cover, guards and loot; validate routes from insertion to extraction; save drafts and publish maps on Nostr. - black-site - map-editor - level-editor - gaming - nostr Needs: identity, outbox, resource, upload · Optional: storage, intent, keys Needs identity, outbox, resource, upload · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njdphx5crqvrrxu6qtvxfaz)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njdphx5crqvrrxu6qtvxfaz)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njdphx5crqvrrxu6qtvxfaz)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njdphx5crqvrrxu6qtvxfaz) **N** ### Naped Snowboarding Publisher [FLX](https://nostr.at/npub1su4kplwca3euuyerm9ucq4ecf7ucxegqmxmjq9v5cudw8l8zk6qqejetyp "872b60fdd8ec73ce1323d9798057384fb9836500d9b7201594c71ae3fce2b680") 2026-10-15 Open-source freestyle snowboarding. Carve, spin, grab and grind for style points on an open mountain, with photographers and a ghost of your best run. - snowboarding - game - sports - physics - 3d No required shell services declared · Optional: identity, outbox, relay, resource, storage, theme, webrtc [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzppetvr7a3mrnecfj8ktesptnsnaesdjspkdhyq2ef3c6u07w9d5qqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznwv9cx2epdwdhx7acxumwml) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzppetvr7a3mrnecfj8ktesptnsnaesdjspkdhyq2ef3c6u07w9d5qqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznwv9cx2epdwdhx7acxumwml)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzppetvr7a3mrnecfj8ktesptnsnaesdjspkdhyq2ef3c6u07w9d5qqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznwv9cx2epdwdhx7acxumwml)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzppetvr7a3mrnecfj8ktesptnsnaesdjspkdhyq2ef3c6u07w9d5qqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznwv9cx2epdwdhx7acxumwml)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzppetvr7a3mrnecfj8ktesptnsnaesdjspkdhyq2ef3c6u07w9d5qqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznwv9cx2epdwdhx7acxumwml) **W** ### WoTformer Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-14 An 8-bit platformer on the TV, your Web of Trust as the enemies, phones as controllers over the mesh Needs: mesh · Optional: identity, outbox, relay, resource Needs mesh · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzthda6xvmmjd4jhyqlrths)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzthda6xvmmjd4jhyqlrths)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzthda6xvmmjd4jhyqlrths)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzthda6xvmmjd4jhyqlrths) **A** ### AppStore Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-14 An app store for napplets: browse the feed and see what people around you have. Needs: mesh, outbox, theme Needs mesh, outbox, theme · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9ekxmmkv4eqf4drj2)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9ekxmmkv4eqf4drj2)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9ekxmmkv4eqf4drj2)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9ekxmmkv4eqf4drj2) **P** ### Pünktlich! Publisher [8494cb65…55fe12](https://nostr.at/npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt "8494cb6579081c56693b4668968b92f75722c19a03152841f49a85a1ff55fe12") 2026-10-14 Klaus, the most German man alive, must catch the 7:00:00 train — not 7:00:01. A 60-second runner: jump garden gnomes, beer crates and Dackels, sort trash into six bins, wait at every red light, get Formular A38 stamped in triplicate, and never jump in the Ruhezone. - game - runner - arcade - germany - humor No required shell services declared · Optional: storage, config, keys [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xvvtpv56rvd3sxvcsjkxks0) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xvvtpv56rvd3sxvcsjkxks0)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xvvtpv56rvd3sxvcsjkxks0)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xvvtpv56rvd3sxvcsjkxks0)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943xvvtpv56rvd3sxvcsjkxks0) **C** ### City Breaker Publisher [8494cb65…55fe12](https://nostr.at/npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt "8494cb6579081c56693b4668968b92f75722c19a03152841f49a85a1ff55fe12") 2026-10-14 1968, a Cold War gone hot. You command a Soviet long-range bomber regiment with 60 days to break Hafenstadt, an industrial city of 1.2 million that repairs, disperses, builds decoys and adapts to your tactics. Plan each raid — targets, launch hour, route, package, bomb loads — then fly it yourself: open the bomb doors, beam the SAMs, fight engine fires and walk your bombs onto the target through flak, searchlights and tracer curtains. Strategy sim plus top-down bomber action, inspired by Hamburg, Hanoi and Desert Storm. The horror is in the numbers. - game - strategy - simulation - flight - war No required shell services declared · Optional: storage, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943kvwrz8q6nsd34xdjsxst343) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943kvwrz8q6nsd34xdjsxst343)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943kvwrz8q6nsd34xdjsxst343)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943kvwrz8q6nsd34xdjsxst343)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw943kvwrz8q6nsd34xdjsxst343) **B** ### Big Rig Rush Publisher [8494cb65…55fe12](https://nostr.at/npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt "8494cb6579081c56693b4668968b92f75722c19a03152841f49a85a1ff55fe12") 2026-10-14 Top-down driving chaos in a living city. Pick your ride: a manual 8-speed semi, a sports car with missiles, a bulldozer that pushes through buildings, or an A-10 Warthog with bombs and a 30 mm cannon. Haul cargo across downtown, the hood, the port and the countryside through day and night while police, fire, ambulance, tow and road crews clean up every crash you cause. Everything is destructible. Keyboard or touch controls. - game - driving - arcade - sandbox - destruction No required shell services declared · Optional: storage, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cxzwfevccxxct9xscqa0qgrg) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cxzwfevccxxct9xscqa0qgrg)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cxzwfevccxxct9xscqa0qgrg)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cxzwfevccxxct9xscqa0qgrg)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cxzwfevccxxct9xscqa0qgrg) **S** ### Supersonic RC Revive Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-13 A remake of the Shockwave RC-car game SuperSonic RC, rebuilt with three.js and unlimited Free Driving. Drive with keyboard, gamepad or touch. Plug in up to four controllers for local split-screen, or play online in public rooms or with friends by private code. Online players use matching asset packs and independent local physics. Load another .ssrcpack or custom level from the title screen. - game - supersonicrc - racing - rc-car - driving Needs: resource · Optional: fs, keys, inc, config, common, identity, webrtc, cvm · 4 pinned versions Needs resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq8y2m42)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq8y2m42)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq8y2m42)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq8y2m42) **B** ### bitchat Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Chat with whoever is near you, over the mesh. No internet, no servers. Needs: mesh · Optional: identity, storage, theme Needs mesh · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmzd96xx6rpwsv4qges)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmzd96xx6rpwsv4qges)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmzd96xx6rpwsv4qges)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqpmzd96xx6rpwsv4qges) **D** ### DingDong Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 One button. Press it and every phone nearby rings. Needs: mesh · Optional: identity, theme, link Needs mesh · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9hxwer0dens69qj24)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9hxwer0dens69qj24)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9hxwer0dens69qj24)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzryd9hxwer0dens69qj24) **S** ### Simple Profile Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 A Nostr profile: who someone is, how many they follow, and Follow. - profile Needs: outbox, relay · Optional: identity, resource, theme, inc, link Needs outbox, relay · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtnd9khqmr9wpex7enfd3jsjnkns8)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtnd9khqmr9wpex7enfd3jsjnkns8)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtnd9khqmr9wpex7enfd3jsjnkns8)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtnd9khqmr9wpex7enfd3jsjnkns8) **N** ### Noris Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Read long-form Nostr articles, and highlight them. - article - highlight Needs: outbox, relay · Optional: identity, resource, theme, inc, intent, link, local, storage Needs outbox, relay · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptwdaexjucvfelwv)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptwdaexjucvfelwv)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptwdaexjucvfelwv)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqptwdaexjucvfelwv) **I** ### Incident Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Start or join an incident with the people around you, agree who leads, and open the tools to work it — no internet needed. - incident Needs: mesh · Optional: theme, intent, inc, identity, storage Needs mesh · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrfde3kjer9de6qhh7p5u)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrfde3kjer9de6qhh7p5u)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrfde3kjer9de6qhh7p5u)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrfde3kjer9de6qhh7p5u) **I** ### Incident Command Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Orders, an activity log and who reports to whom, for an incident — over the mesh, no internet needed. - incident-command Needs: mesh · Optional: theme, intent, inc, identity, storage Needs mesh · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmfvdesvg8qzg)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmfvdesvg8qzg)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmfvdesvg8qzg)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqqmfvdesvg8qzg) **D** ### Doc Review Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Review the writing in a Nostr git pull request: tracked changes, comments and suggested edits, made for tablets and e-paper. Needs: identity, outbox, relay, resource · Optional: link, storage, theme Needs identity, outbox, relay, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztyda3hyetkd9jhw62wtpr)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztyda3hyetkd9jhw62wtpr)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztyda3hyetkd9jhw62wtpr)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqztyda3hyetkd9jhw62wtpr) **C** ### Composer Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Write a Nostr note: a new one, a reply, or a quote. - composer Needs: identity, outbox, relay · Optional: resource, theme, inc, intent, link, storage Needs identity, outbox, relay · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrrdakhqmmnv4eqkzsp40)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrrdakhqmmnv4eqkzsp40)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrrdakhqmmnv4eqkzsp40)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqzrrdakhqmmnv4eqkzsp40) **C** ### Chronofeed Publisher [Arjen](https://nostr.at/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747") 2026-10-13 Notes and reposts from the people you follow, newest first, nothing ranked. - note Needs: identity, outbox, relay · Optional: resource, theme Needs identity, outbox, relay · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznrdpex7mn0vejk2eqcqw6ry)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznrdpex7mn0vejk2eqcqw6ry)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznrdpex7mn0vejk2eqcqw6ry)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpwa4mkswz4t8j70s2s6q00wzqv7k7zamxrmj2y4fs88aktcfuf68qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqznrdpex7mn0vejk2eqcqw6ry) **C** ### Controller tester Publisher [16b7a947…e484b2](https://nostr.at/npub1z6m6j3lzckd926gz4exwm7hpgpv24dwr3l3fx0anxqru0a8ysjeqn69npz "16b7a947e2c59a556902ae4cedfae14058aab5c38fe2933fb33007c7f4e484b2") 2026-10-13 Inspect shell-delivered controller input and verify focus isolation. Needs: gamepad · Optional: theme Needs gamepad · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzq94h49r793v6245s9tjvahawzszc426u8rlzjvlmxvq8cl6wfp9jqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8v9kk2urpvskhgetnwsvs7cus)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzq94h49r793v6245s9tjvahawzszc426u8rlzjvlmxvq8cl6wfp9jqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8v9kk2urpvskhgetnwsvs7cus)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzq94h49r793v6245s9tjvahawzszc426u8rlzjvlmxvq8cl6wfp9jqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8v9kk2urpvskhgetnwsvs7cus)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzq94h49r793v6245s9tjvahawzszc426u8rlzjvlmxvq8cl6wfp9jqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8v9kk2urpvskhgetnwsvs7cus) **B** ### Browser Side Quest Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-12 Browser Side Quest revives Mozilla's 2012 pixel-art adventure BrowserQuest as a napplet. Explore the village, beach, forest, desert and lava caves; fight rats, skeletons and the final boss; loot better swords and armour; and unlock all 20 achievements. Up to eight adventurers share a world peer to peer, under their Nostr names and profiles, or you can play solo. It plays with mouse, touch, keyboard or gamepad. - game - rpg - multiplayer - adventure - mmo No required shell services declared · Optional: identity, storage, common, resource, webrtc, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ux2dtpvfnrjc34xgmqe9fukf) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ux2dtpvfnrjc34xgmqe9fukf)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ux2dtpvfnrjc34xgmqe9fukf)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ux2dtpvfnrjc34xgmqe9fukf)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ux2dtpvfnrjc34xgmqe9fukf) **W** ### Wizard Duel Publisher [Nose-whistle](https://nostr.at/npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2 "993dfad40fef286b323b9217a15a144bf2a2372987c159deea665462051856c0") 2026-10-11 Two wizards share one keyboard and fling silly spells at each other on a moonlit castle rooftop. Tap rune combos to cast 28 spells, most of them secret. - game - two-player - wizards - pixel-art No required shell services declared · Optional: config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95urvvpsx4jrwdt9vg6q0pgc2r) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95urvvpsx4jrwdt9vg6q0pgc2r)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95urvvpsx4jrwdt9vg6q0pgc2r)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95urvvpsx4jrwdt9vg6q0pgc2r)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95urvvpsx4jrwdt9vg6q0pgc2r) **S** ### Sickman Survival Publisher [hzrd149](https://nostr.at/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5") 2026-10-11 Outlast an escalating meteor storm as a stickman on a randomly generated, fully destructible world of cliffs, houses, trees, bouncy mushrooms and caves full of explosive moles. Play solo with keyboard, touch or gamepad, or plug in more controllers for up to four-player split screen. - game - platformer - physics - survival - arcade No required shell services declared · Optional: storage, config [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ursvp5xy6nzerxxanqee2jfk) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ursvp5xy6nzerxxanqee2jfk)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ursvp5xy6nzerxxanqee2jfk)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ursvp5xy6nzerxxanqee2jfk)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95ursvp5xy6nzerxxanqee2jfk) **G** ### Gameboy Color Emulator Publisher [emukid](https://nostr.at/npub1emukld86r62fcl3pt27t9wu4hh60rkkwmucm99wj5wp2aq5enpxqjc9ys6 "cef96fb4fa1e949c7e215abcb2bb95bdf4f1dacedf31b295d2a382ae8299984c") 2026-10-11 A GameBoy Color emulator based on WasmBoy - gameboy Needs: identity, outbox, relay, resource · Optional: keys, inc, intent, fs Needs identity, outbox, relay, resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpnhed7605855n3lzzk4uk2aet00578dvahe3k22a9guz46pfnxzvqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8vf3j6etdw4kxzar0wgumfs5y)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpnhed7605855n3lzzk4uk2aet00578dvahe3k22a9guz46pfnxzvqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8vf3j6etdw4kxzar0wgumfs5y)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpnhed7605855n3lzzk4uk2aet00578dvahe3k22a9guz46pfnxzvqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8vf3j6etdw4kxzar0wgumfs5y)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpnhed7605855n3lzzk4uk2aet00578dvahe3k22a9guz46pfnxzvqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrr8vf3j6etdw4kxzar0wgumfs5y)[Source ↗](https://github.com/emukid/gbcolor-napplet) **S** ### super-platformer Publisher [d73ae26c…28fe01](https://nostr.at/npub16uawymqtmdg4j6zcl6snt5apzfp4ek2g8lpasnqa75tspdpglcqsgpatvc "d73ae26c0bdb51596858fea135d3a112435cd9483fc3d84c1df51700b428fe01") 2026-10-07 super-platformer No required shell services declared · 2 pinned versions [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzp4e6ufkqhk63t9593l4pxhf6zyjrtnv5s07rmpxpmaghqz6z3lspqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95crzv3c8p3rzctzxvuqkdt3t3) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzp4e6ufkqhk63t9593l4pxhf6zyjrtnv5s07rmpxpmaghqz6z3lspqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95crzv3c8p3rzctzxvuqkdt3t3)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzp4e6ufkqhk63t9593l4pxhf6zyjrtnv5s07rmpxpmaghqz6z3lspqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95crzv3c8p3rzctzxvuqkdt3t3)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzp4e6ufkqhk63t9593l4pxhf6zyjrtnv5s07rmpxpmaghqz6z3lspqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95crzv3c8p3rzctzxvuqkdt3t3)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzp4e6ufkqhk63t9593l4pxhf6zyjrtnv5s07rmpxpmaghqz6z3lspqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95crzv3c8p3rzctzxvuqkdt3t3) **M** ### mini-nostr-saf Publisher [75e0f2d4…a4a431](https://nostr.at/npub1whs094x2p7qshzqn77uwm0kx0826f8xdad5ppqet7w9jr6ay5scs99c7wj "75e0f2d4ca0f810b8813f7b8edbec679d5a49ccdeb6810832bf38b21eba4a431") 2026-10-07 mini-nostr-saf: a minimal Nostr safari client. Browse notes, follows and long-form posts; the host signs with your identity, keys never touch the app. - nostr - client - social No required shell services declared · 2 pinned versions [Run here](https://napplet.soy/n/naddr1qvzqqqyf8ypzqa0q7t2v5ruppwyp8aacaklvv7w45jwvm6mgzzpjhuuty846ffp3qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njwfkxyuxvd3cxseqpullfg) Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzqa0q7t2v5ruppwyp8aacaklvv7w45jwvm6mgzzpjhuuty846ffp3qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njwfkxyuxvd3cxseqpullfg)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzqa0q7t2v5ruppwyp8aacaklvv7w45jwvm6mgzzpjhuuty846ffp3qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njwfkxyuxvd3cxseqpullfg)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzqa0q7t2v5ruppwyp8aacaklvv7w45jwvm6mgzzpjhuuty846ffp3qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njwfkxyuxvd3cxseqpullfg)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzqa0q7t2v5ruppwyp8aacaklvv7w45jwvm6mgzzpjhuuty846ffp3qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw956njwfkxyuxvd3cxseqpullfg) **S** ### Supersonic RC Revive Publisher [sandwich](https://nostr.at/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f") 2026-10-04 A faithful remake of the Shockwave RC-car game SuperSonic RC, rebuilt with three.js. The car handling is ported line by line from the original Lingo scripts, with all five modes: Free Driving, Checkpoint Chase, Stunt Rally, Scavenger Hunt and the Supersonic Challenge. Drive with the arrow keys or WASD, a gamepad, or the on-screen touch controls. The art and sound are a recreated asset pack, and you can load another .ssrcpack from the title screen. - game - racing - rc-car - driving - remake Needs: resource Needs resource · open it in a shell Open in[napplet.soy ↗](https://napplet.soy/n/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq7d02gw)[Paja ↗](https://kehto.github.io/web/paja/?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq7d02gw)[Applesauce loader ↗](https://applesauce.build/examples/#/example/napplet/loader?pointer=naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq7d02gw)[Signed release ↗](https://njump.me/naddr1qvzqqqyf8ypzpem34u9stj8ftlxldl4n2qz5f5hmrnxns3uga86fpwe7u28ga4n0qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0wfjkccte9ejxjar5duh8qatzqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpz4mhxue69uhhyetvv9ujumn0wd68ytnrdaksqrtw95cngve3xsmxyvryxenq7d02gw) About this collection Discovery uses NIP-5D’s own manifest kinds, verifies publisher signatures, and selects releases with a direct artifact-hash tag and a plain-text description. Legacy aggregate-hash releases stay out of this feed. A signature identifies a publisher; it does not review the code or certify every manifest field. Some publishers still use source-location formats outside the current proposal. Your chosen shell verifies artifacts and decides which services it can provide. Artwork comes from the manifest first: icons and screenshots are fetched by hash from the publisher’s declared Blossom servers and shown only after the bytes verify and decode, so they need JavaScript. In this snapshot 62 of 96 releases declare usable manifest media. When a manifest has none, a Software Applications listing (kind 32267) from the same publisher that names the release supplies listing art by URL; 48 releases have one. That pairing is a display convenience of this site, not part of either proposal. The publisher line links each signing key to its profile on nostr.at and shows the name and picture from the key’s kind 0 profile when the same relays hold one (63 of 96 releases here); the key identifies the publisher, the name only labels it. When a listing supplies no artwork and the manifest supplies no screenshot whose bytes verify, a [Napshot](https://github.com/napplet/napshot) preview may fill in: an automated capture published as a signed NIP-32 label that names the exact release revision. The label’s signed content states how the capture was taken, and the visible badge on the image repeats it. Only labels from the Napshot signer this site trusts are used, and the image is shown only after its bytes match the labelled hash, size and dimensions; its mirrors must be HTTPS and must not redirect. 46 releases in this snapshot use one in place of missing publisher art. A release still shown without artwork asks the same relays for a label naming its exact revision when its card comes into view, so a capture published after this snapshot was saved appears without a discovery pass. A preview shows who captured it; it is not an endorsement, and apps that need relays or an account may look empty in it. The snapshot reads wss://relay.napplet.soy and wss://relay.damus.io and wss://relay.ditto.pub and wss://relay.primal.net and wss://relay.nostr.com and shows twelve releases at a time as you scroll. Past the saved collection, the same relays are reconciled with NIP-77 negentropy so only releases this page lacks are downloaded; a relay without NIP-77 is paged with ordinary requests instead. [Read the living NIP-5D proposal ↗](https://github.com/nostr-protocol/nips/pull/2303) · [Software Applications proposal ↗](https://github.com/nostr-protocol/nips/pull/1336) · [Find another shell ↗](https://napplet.run/#shells) [Room for one more.**Show us what you made ↗**](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt) --- NIP-5D · Nostr Web Applets # Composable Nostr apps, not monoliths. A **Nostr Applet** — a **napplet** — is a small, sandboxed app that does one thing well. It runs inside a host **shell** and delegates signing, storage and relay access over a simple, typed message protocol — so apps stop rebuilding the same client, and every sensitive request is brokered by the shell. [Get started](https://napplet.run/explainer/#start) [Read NIP-5D ↗](https://github.com/nostr-protocol/nips/pull/2303) [Join community ↗](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt) [Documentation](https://napplet.run/docs/) sandboxed & isolated by the host · every request via the shell · any shell, any napplet **Web first — not web only** web napplets prove the protocol on the web by choice - A real, browser-enforced sandbox - The biggest developer ecosystem - Ships as a NIP-5A nsite — no servers The napplet & [**NAP**](https://napplet.run/protocol/) model is transport-agnostic by design — room to grow beyond the browser. Projections one capability seam, many hosts — see the [registry](https://napplet.run/protocol/) | Projection | Binding | Status | | --- | --- | --- | | Web | `iframes + postMessage · window.napplet.*` | In use | | Native | `OS process + IPC / FFI` | Possible | | WASM | `host imports` | Possible | wtf is an applet? ## applet /ˈæp.lɪt/ noun a small application built to do *one* narrow task — run *inside* a larger host program rather than launched on its own. Blend *application* with the diminutive *\-let* and you get a tiny program made to live inside a bigger one. The word turns up in print [around 1990](https://www.merriam-webster.com/dictionary/applet), then gets burned into a generation’s memory by [Java applets](https://en.wikipedia.org/wiki/Java_applet) running sandboxed in the browser — though the instinct of shipping a little program to extend a remote machine goes all the way back to [RFC 5](https://www.rfc-editor.org/rfc/rfc5.html) in 1969. A **napplet** is that same idea, reborn Nostr-native. The problem ## Nostr clients are monoliths The protocol is composable. The clients are not. Each one is a giant app that owns your keys and reinvents the same building blocks — so good ideas stay trapped inside whichever client shipped them. ### Everyone rebuilds everything Feeds, DMs, profiles, relay management, search — every client re-implements the same surface from scratch. ### Your key is everywhere Each app you install asks for direct signing access. The more apps that hold that power, the larger your exposure. ### No real composition You can’t drop one team’s great feed into another team’s great messenger. Features are welded to clients. ### All-or-nothing trust Installing a client means trusting the whole thing — its code, its relays, its handling of your data. The shift ## Invert the client NIP-5D splits the monolith in two. A trusted **shell** brokers every sensitive request — signing, relays, storage. Focused **web napplets** render the UI and ask the shell to do that work. Same protocol — completely different shape. Today ### One monolith, rebuilt every time nostr-client FeedDMsProfilesRelaysSigningStorageSearchZapsSettingsMediaNotifsLists Signing, relays & UI all fused together. Every client re-implements the same surface — and wires up your signer and relays itself. With napplets ### A shell hosts swappable napplets shell sandbox Feed sandbox Chat sandbox Profile sandbox Relays mediation signing relays storage Each napplet does one thing, sandboxed. The shell brokers every sensitive call — to a signer, extension or relay. Swap napplets like apps. How it works ## A message bus with a trust boundary Every sensitive request goes through the shell, which brokers it to a signer, extension or relay. Web napplets just send envelopes and get answers back. On the web — the first target — that boundary is an `iframe` and `postMessage`. sandboxed iframe napplet `window.napplet.relay.subscribe()` trusted host shell identity via source ACL check NAP dispatch nostr relays shell mediates 01 ### One envelope, every message Napplet and shell speak a single JSON wire format: { type: "domain.action", ...payload } over postMessage. No SDK lock-in — it’s just messages. 02 ### Identity bound to the document The verified app hands the shell a private channel before any of its code runs. If the frame loads anything else, the shell cuts the channel and drops the app. 03 ### Sandboxed by construction Iframes use allow-scripts only — no allow-same-origin. Napplets can’t read shell storage, cookies or keys. Everything sensitive is proxied. 04 ### Capabilities, negotiated A runtime injects window.napplet.relay when relay access is available. NAP domains carve the protocol into modular capabilities. Benefits ## Why the pattern wins Splitting trust from UI isn’t just tidy — it changes what’s safe, what’s reusable, and who’s in control. ### Every request goes through the shell Napplets never call a signer, relay or store directly. They ask the shell, which brokers each request — to a remote signer, an extension, or its own services — and can say no. ### Real sandbox isolation allow-scripts without allow-same-origin: no service workers, no shell storage, no cross-origin reach. Each napplet does one thing, so its surface and permissions stay small. ### Compose, don’t rebuild Drop a great feed next to a great messenger. Napplets are interchangeable parts, not features welded into one app. ### A bigger pool of builders The hard Nostr protocol lives behind the shell. Developers can ship a useful napplet without deep protocol knowledge — and without putting users at risk. ### The user stays in control The shell brokers every relay, storage and signing request, and scopes storage per napplet. Consent lives with the host, not the app. ### Portable across shells Write to NIP-5D, run in any compatible shell. No vendor SDK lock-in — the protocol is the contract. The paradigm shift ## An app store, not an app Treat the shell like an operating system for Nostr. Napplets become installable, swappable programs — and the ecosystem stops being a handful of monoliths. Clients compete on features Shells compete on trust & UX When any napplet can run anywhere, a shell wins by being safer and nicer to use — not by hoarding features. Pick one app, get its whole worldview Assemble your own client Mix a feed from one team, a wallet from another, a niche tool from a solo dev. Your client becomes a composition. Deep Nostr expertise to ship anything One dev ships one great napplet The hard protocol lives behind the shell. Render UI, send envelopes — no relay pools, NIP-46 signers or event-signing to get right. A far broader pool of builders can contribute. Trust the whole client with your signer Trust the shell, sandbox the rest A single audited host brokers every sensitive request. Everything else runs contained, asking the shell for what it needs. Runtimes & shells ## Any shell can be a runtime A **runtime** is the host that actually runs your web napplet — it brokers your requests to a signer, extension or relay and talks to the network. On the web, a runtime is any app that honors **NIP-5D** — the security posture and transport for web napplets — and there can be many. That’s the point. Your napplet UI + product logic — the one thing it does well @napplet/shim · @napplet/sdk window.napplet global, or named exports for bundlers NIP-5D envelope { type: "domain.action", ...payload } over postMessage Shell / runtime Brokers every request, enforces ACL, talks to signers & relays [Kehto — reference runtime ↗](https://github.com/kehto/web) Nostr network Relays, signers, the wider protocol For client developers ### Already built a Nostr client? You’ve solved the hard parts — relay pools, signer flows, storage, key handling. Expose them through NAP interfaces and your client becomes a shell. Host third-party web napplets without rewriting your app, and turn your client into a platform. - Reuse your existing relay & signer plumbing - Host an ecosystem instead of shipping every feature - Compete on trust & UX, not feature count For protocol developers ### Want to shape the protocol? NIP-5D is small and the surface is well-defined. Stand up a runtime, implement the NAP capability domains, and help decide what the protocol becomes. Conformance work and new NAPs are where the design is still being written. - Implement NIP-5D — transport, identity, dispatch - Define & extend NAP capability domains - Drive conformance against the reference runtime [Browse the NAPs track ↗](https://napplet.run/protocol/) Reference runtime ### Kehto implements NIP-5D end-to-end The reference shell where the protocol runs for real — host a napplet, watch the envelopes flow, and use it as the conformance target while you build a runtime of your own. [Explore Kehto ↗](https://github.com/kehto/web) [How shells work](https://napplet.run/docs/) Packages ## The napplet SDK Small, ESM-only, zero-framework packages. Import the shim for a window global, the SDK for named exports, or pull a single NAP domain. Alpha tooling The development ecosystem is alpha. Packages can break, tooling paths may be incomplete, and the living spec can drift under active implementation. [ `@napplet/core` JSON envelope types and NAP dispatch infrastructure shared by the SDK packages. Protocol requirements live in NIP-5D and the NAP specs. Docs · npm · JSR ](https://napplet.run/docs/packages/core.html)[ `@napplet/shim` Runtime-side domain installers for the window.napplet namespace. Shells inject the available domains before napplet scripts run. Docs · npm · JSR ](https://napplet.run/docs/packages/shim.html)[ `@napplet/sdk` Named TypeScript exports wrapping window.napplet for bundler consumers — relay, inc, services, storage objects plus NAP message types. Docs · npm · JSR ](https://napplet.run/docs/packages/sdk.html)[ `@napplet/nap` All NAP domains as layered subpath exports — barrel, types-only, shim-only, or sdk-only. Tree-shakable. Import from a domain subpath. Docs · npm · JSR ](https://napplet.run/docs/packages/nap.html)[ `@napplet/vite-plugin` Build-time napplet manifest generation. Hashes each file and signs a kind 35129 manifest event (NIP-5A tag schema). Docs · npm · JSR ](https://napplet.run/docs/packages/vite-plugin.html)[ `@napplet/cli` Standalone CLI for creating projects, owning deploy metadata, previewing in Paja, and publishing NIP-5A manifests to Blossom and Nostr relays. Docs · npm · JSR ](https://napplet.run/docs/packages/cli.html)[ `@napplet/conformance` Framework-agnostic protocol conformance engine — reference mock shell, per-NAP envelope validators, a manifest validator, and reporters. A dev/test tool, not loaded in the sandbox. Docs · npm · JSR ](https://napplet.run/docs/packages/conformance.html)[ `@napplet/conformance-cli` The headless napplet-conformance runner. Drives the conformance engine against your napplet in real Chromium via Playwright — wire it up as test:conformance. Docs · npm ](https://napplet.run/docs/packages/conformance-cli.html)[ `@napplet/conformance-web` Browser conformance runtime. Loads a napplet URL, runs the conformance engine live in the page, and powers the deployed /conformance app. Docs · npm ](https://napplet.run/docs/packages/conformance-web.html)[ `@napplet/boilerplate` Project scaffold behind napplet create. Clones the maintained Vite + TypeScript starter without owning deployment metadata. Docs · npm ](https://napplet.run/docs/packages/boilerplate.html) Agent skills Building with a coding agent? The `napplet-*` skills teach it the sandbox boundary, the SDK surface, and the applet UI contract. Install them with the open skills CLI: ``` npx skills add napplet/web ``` [Skill reference ↗](https://napplet.run/docs/guide/agent-skills.html) Alpha software ### Before you install napplet is alpha. The CLI and tooling could be broken or incomplete, and the living specification could drift as it evolves. --- A different shape for software # Apps that travel. Build a game, a tool, a small obsession. Give it more places to run. [Try one](https://napplet.run/#napplets) [Make something](https://napplet.run/#build) [How it works](https://napplet.run/#shells) Web technology. Open protocol.[Follow the app](https://napplet.run/#napplets) 01 / Start here ## Less explaining. More playing. These are real napplets. Open one right here, then look under the hood. ![Freeze Frame Response interface](https://napplet.run/showcase/images/freeze-frame-response.webp) Physics puzzle ### Freeze Frame Response Freeze the blast. Place trampolines, funnels, and cushions. Resume time and see what happens. [Run Freeze Frame Response](https://napplet.soy/n/naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkuttzvy6nyefnxpjr2e3jqg5uk8)[Source](https://blossom.napplet.soy/e747ab4f9e35a11fd11bd9cf153edd9d49a1ce20debbbe6910c7df27e2e9924e) ![Stick Brawl interface](https://napplet.run/showcase/images/stick-brawl.webp) Survival game ### Stick Brawl One green stickman. Endless waves of trouble. Punch, dodge, and climb your way through the city. [Run Stick Brawl](https://napplet.soy/n/naddr1qvzqqqyf8ypzpxfalt2qlmegdverhysh59dpgjlj5gmjnp7pt80w5ej5vgz3s4kqqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutf4x56xyvej89jxywfn5x7t9w)[Source](https://blossom.napplet.soy/9794d3420d4d8ef9b3800b7014abb6b742b8e102fea15d8e3787248c4dec524c) ![Poly Dogfight interface](https://napplet.run/showcase/images/poly-dogfight.webp) Flight game ### Poly Dogfight Pilot a biplane around a tiny planet. Thread the rock arches, keep your speed up, and take on a dogfight. [Run Poly Dogfight](https://napplet.soy/n/naddr1qvzqqqyf8ypzqfngzhsvjggdlgeycm96x4emzjlwf8dyyzdfg4hefp89zpkdgz99qythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkutf5x3nrvvm9x56ryvnzrgxw9k)[Source](https://blossom.napplet.soy/fbf8fc78f64d293aa88ebd4e1afb4e9ba668c8621f0e124af4476183c37ecca1) [There’s more out there.**Explore the app store** Curated picks + community discoveries](https://napplet.run/apps/) Original code. Ready to play. [Share what you’re building](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt) 02 / How it works ## The app is yours. The setting changes. A napplet is a web app built on Nostr that runs inside a host called a shell. Your app brings the interface. The shell supplies services such as identity and storage. Different shells offer different capabilities. Find your settingRuns on ### [Amethyst](https://amethyst.social/) Active development A social client with a built-in napplet host, on your phone and your desktop. - Android - macOS - Linux - Windows [Source ↗](https://github.com/vitorpamplona/amethyst) ### [Myco](https://github.com/Origami74/myco) Available Pass apps between nearby phones. Keep opening the apps you have, even without an internet connection. - Android - Android TV [Source ↗](https://github.com/Origami74/myco) ### [noStrudel](https://nostrudel.ninja/) Active development Discover apps on profiles and open napplets alongside your social world. - Web [Source ↗](https://github.com/hzrd149/nostrudel) ### [Grimoire](https://grimoire.rocks/) Active development A tiling workspace for feeds, profiles, protocol exploration, and napplets. - Web [Source ↗](https://github.com/purrgrammer/grimoire) ### [napplet.soy](https://napplet.soy/) Available A playground for tiny games, digital experiments, and things people made just because they could. - Web [Source ↗](https://github.com/zeSchlausKwab/napplet-soy) ### [Paja](https://kehto.github.io/web/paja/) Developer preview Kehto’s developer shell. Load a published app and inspect its behavior. - Web [Source ↗](https://github.com/kehto/web/tree/main/packages/paja) ### [nostrapps](https://nostrapps.com/) Work in progress A browser launcher that fetches nsites, napps and napplets, caches them, and runs each in its own sandboxed window. - Web [Source ↗](https://github.com/eskema/nostrapps) ### [Applesauce loader](https://applesauce.build/examples/#/example/napplet/loader) Example The applesauce napplet loader example: a browser host built on the applesauce libraries, with the store and browser examples beside it. - Web [Source ↗](https://github.com/hzrd149/applesauce/tree/master/apps/examples) Platforms describe each project. App support and available services vary by version and shell. [How shells work ↗](https://napplet.run/explainer/) [Build a home for apps](https://napplet.run/#runtimes) 03 / Open by design ## A common ground. Room to differ. Apps and shells meet through a shared protocol. Build either side. Read the contracts. Propose what comes next. [Take the technical tour](https://napplet.run/explainer/) [01 Core proposal ### NIP-5D How a web app and its shell connect. ](https://github.com/nostr-protocol/nips/pull/2303)[02 Capability directory ### Explore the NAPs What apps can ask their shells to do. ](https://napplet.run/protocol/) An evolving proposal, with independently built implementations. The living specifications define the protocol. 04 / Your part in it ## What will you make? Start with the part you care about. There is more than one way in. 01 ### An app Use the web tools you know. Build by hand, or give your coding agent the skills to get started. [Start building ↗](https://napplet.run/docs/guide/getting-started.html)[Build with an agent ↗](https://napplet.run/docs/guide/agent-skills.html) 02 ### A shell A workspace, a playground, a place in your existing product. Decide how apps fit together. [Explore the runtime toolkit ↗](https://github.com/kehto/web) 03 ### A service Connect a service through a shell. Start with an existing capability contract, or help define a new one. [Find a capability ↗](https://napplet.run/protocol/)[Propose one ↗](https://napplet.run/protocol/contribute/) 04 ### A tool Help people create, publish, discover, or test apps. Build on the open packages and specifications. [Explore the packages ↗](https://napplet.run/docs/packages/) Made something? We’d like to see it. [Meet the builders ↗](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt) --- [← All NAPs](https://napplet.run/protocol/) Contribute / Start with the boundary # One capability. A shared contract. A new NAP makes sense when napplets need a runtime-provided API that the existing capability contracts do not cover. The decision guide below is editorial guidance inferred from the upstream [boundary rule](https://github.com/napplet/naps#boundary-rule). The repository’s current guidance takes precedence. ## Does this need a NAP? Runtime provides an API ### Propose a capability. First check the [merged and proposed NAPs](https://napplet.run/protocol/). Extend an existing domain when it already owns the operation. For a distinct runtime capability, start a NAP proposal. [NAP interface template ↗](https://github.com/napplet/naps/blob/master/NAP-WORD-TEMPLATE.md) Napplets agree on a payload ### Write a convention. If the idea describes what napplets say to one another rather than a runtime API, use a convention. A shared message shape does not by itself need a new NAP. [Convention template ↗](https://github.com/napplet/naps/blob/master/CONVENTION-TEMPLATE.md) A role needs a name ### Propose an archetype. If the idea names what kind of napplet this is and where its responsibility ends, it belongs in the NAAT registry. An archetype owns neither an API nor a payload. [Archetype template ↗](https://github.com/napplet/naps/blob/master/naat/TEMPLATE.md) A host carries the contract ### Discuss the projection. Transport and host bindings belong to a projection. Keep the capability contract separate from how a browser or another host delivers it. [Web projection ↗](https://github.com/napplet/naps/blob/master/projections/web.md) ## From idea to pull request 1. ### Check the current conversation. Search the directory and open PRs for overlap. Describe the missing capability, which napplets need it, and what the runtime would provide. 2. ### Start from the upstream template. Fork [napplet/naps](https://github.com/napplet/naps), make a branch for the spec, and add `naps/NAP-WORD.md` using the [current interface template](https://github.com/napplet/naps/blob/master/NAP-WORD-TEMPLATE.md). Keep one spec concern in the PR. 3. ### Make the contract reviewable. Describe operations, schemas, errors, dependencies, runtime behavior, and security boundaries. Follow the template’s language-neutral tables and link to the living projection specifications. Read the [contributor guide](https://github.com/napplet/naps/blob/master/AGENTS.md) for the current dependency and changelog rules. 4. ### Open the discussion. Use a title such as `NAP-WORD: Add a focused capability`. Explain the motivation and concrete changes. Keep the registry entry and related docs consistent, and link an implementation when available. 5. ### Refine it with the community. Discuss the proposal in PR comments, respond to interoperability concerns, and update the spec as the design stabilizes. Maintainers decide when to merge; an open PR is not acceptance. Based on upstream [governance](https://github.com/napplet/naps#governance) and the [contributor guide](https://github.com/napplet/naps/blob/master/AGENTS.md). Names and acceptance are maintained by that repository. Refreshed from upstream ## The current template These are the headings in the current interface template. Open it for the latest instructions and examples. [Use the NAP template ↗](https://github.com/napplet/naps/blob/master/NAP-WORD-TEMPLATE.md) - Description - API Surface - Schemas - Wire Protocol - Examples - Error Handling - Shell Behavior - Security Considerations - Implementations Current upstream boundary and governance notes A NAP is runtime-provided AND defines an API surface. A convention is napplet-agreed AND defines message semantics. Only runtime-provided API surfaces are NAPs. - NIP-style informal process: - Fork this repo, add a markdown file under naps/ following the interface template, open a PR. Every NAP spec is a named runtime capability contract. Templates and the NAP registry (README.md) stay at the repo root. - Community discusses via PR comments. - Maintainers merge when the spec has been implemented, defended and has stabilized. - No formal stages, review committees, or voting. - NAP-WORD names are first-come-first-served but must be approved by the maintainer. [Read these in context on GitHub ↗](https://github.com/napplet/naps#governance) --- An open foundation / Protocol # Small apps. Shared contracts. NAPs describe the capabilities a runtime offers a napplet. Explore the specifications, see what’s being proposed, and help shape what comes next. [Explore the NAPs ↓](https://napplet.run/protocol/#directory)[Propose a NAP →](https://napplet.run/protocol/contribute/) [01 / Identity ## NIP-5D ↗ The manifest that describes a napplet. ](https://github.com/nostr-protocol/nips/pull/2303)[02 / Web projection ## NIP-5D ↗ The proposed web binding between applet and shell. ](https://github.com/nostr-protocol/nips/pull/2303)[03 / Capabilities ## NAPs ↗ The contracts, their registry, and open discussion. ](https://github.com/napplet/naps) The capability directory ## Merged & in motion. **5** merged · **46** proposed changes “Merged” means a spec file exists on the repository’s default branch. Open and draft PRs are proposals, including amendments. Upstream status is shown separately; deferred proposals are dormant. Shipped implementation maturity is a domain-wide package assessment, not proof that every amendment is implemented. All capabilities remain subject to change. Merged NAP specifications and open NAP pull requests | NAP / domain | Capability | Repository state | Upstream status | Shipped implementation | | --- | --- | --- | --- | --- | | [NAP-IDENTITY](https://napplet.run/protocol/nap-identity/) identity | [Read-Only User Identity Queries](https://napplet.run/protocol/nap-identity/) | Merged | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-INC](https://napplet.run/protocol/nap-inc/) inc | [Inter-Napplet Communication](https://napplet.run/protocol/nap-inc/) | Merged | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-INTENT](https://napplet.run/protocol/nap-intent/) intent | [Archetype Intent Dispatcher](https://napplet.run/protocol/nap-intent/) | Merged | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-SHELL](https://napplet.run/protocol/nap-shell/) shell | [Optional Shell Environment and Readiness](https://napplet.run/protocol/nap-shell/) | Merged | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-THEME](https://napplet.run/protocol/nap-theme/) theme | [Shell-Provided Theming](https://napplet.run/protocol/nap-theme/) | Merged | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-BLE](https://napplet.run/protocol/nap-ble-pr-62/) ble | [Runtime-Mediated Bluetooth Low Energy](https://napplet.run/protocol/nap-ble-pr-62/) | Open PRPR #62 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-BLOSSOM](https://napplet.run/protocol/nap-blossom-pr-71/) blossom | [Low-Level Blossom Blob Access](https://napplet.run/protocol/nap-blossom-pr-71/) | Open PRPR #71 | Not listedDocument: draft | Not shipped | | [NAP-CATALOG](https://napplet.run/protocol/nap-catalog-pr-95/) catalog | [Runtime Napplet Catalog](https://napplet.run/protocol/nap-catalog-pr-95/) | Open PRPR #95 | Not listedDocument: draft | Not shipped | | [NAP-COMMON](https://napplet.run/protocol/nap-common-pr-67/) common | [Common Social Actions](https://napplet.run/protocol/nap-common-pr-67/) | Open PRPR #67 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-COUNT](https://napplet.run/protocol/nap-count-pr-69/) count | [Runtime-Mediated Event Counts](https://napplet.run/protocol/nap-count-pr-69/) | Open PRPR #69 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-CVM](https://napplet.run/protocol/nap-cvm-pr-31/) cvm | [ContextVM Bridge](https://napplet.run/protocol/nap-cvm-pr-31/) | Open PRPR #31 | DraftDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-FS](https://napplet.run/protocol/nap-fs-pr-88/) fs | [Virtual Filesystem Access](https://napplet.run/protocol/nap-fs-pr-88/) | Open PRPR #88 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-GAMEPAD](https://napplet.run/protocol/nap-gamepad-pr-130/) gamepad | [Shell-Mediated Gamepad Input](https://napplet.run/protocol/nap-gamepad-pr-130/) | Open PRPR #130 | Not listedDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-GIT](https://napplet.run/protocol/nap-git-pr-137/) git | [Runtime-Mediated Git Repositories](https://napplet.run/protocol/nap-git-pr-137/) | Open PRPR #137 | Not listedDocument: draft | Not shipped | | [NAP-INTENT](https://napplet.run/protocol/nap-intent-pr-106/) intent | [Archetype Intent Dispatcher](https://napplet.run/protocol/nap-intent-pr-106/)Proposed amendment | Open PRPR #106 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-INTENT](https://napplet.run/protocol/nap-intent-pr-128/) intent | [Archetype Intent Dispatcher](https://napplet.run/protocol/nap-intent-pr-128/)Proposed amendment | Open PRPR #128 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-KEYS](https://napplet.run/protocol/nap-keys-pr-9/) keys | [Keyboard Forwarding and Action Keybindings](https://napplet.run/protocol/nap-keys-pr-9/) | Open PRPR #9 | DraftDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-LINK](https://napplet.run/protocol/nap-link-pr-53/) link | [Shell-Mediated Link Opening](https://napplet.run/protocol/nap-link-pr-53/) | Open PRPR #53 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-LISTS](https://napplet.run/protocol/nap-lists-pr-68/) lists | [Runtime-Mediated Nostr List Mutation](https://napplet.run/protocol/nap-lists-pr-68/) | Open PRPR #68 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-LLM](https://napplet.run/protocol/nap-llm-pr-134/) llm | [User-selected language models](https://napplet.run/protocol/nap-llm-pr-134/) | Open PRPR #134 | Not listedDocument: draft | [exploratory](https://napplet.run/docs/naps/maturity.html) | | [NAP-MEDIA](https://napplet.run/protocol/nap-media-pr-10/) media | [Media Session Control](https://napplet.run/protocol/nap-media-pr-10/) | Open PRPR #10 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-NAVIGATION](https://napplet.run/protocol/nap-navigation-pr-126/) navigation | [Runtime-Maintained Intent History](https://napplet.run/protocol/nap-navigation-pr-126/) | Open PRPR #126 | Not listedDocument: draft | Not shipped | | [NAP-NOTIFY](https://napplet.run/protocol/nap-notify-pr-11/) notify | [Shell-Rendered Notifications](https://napplet.run/protocol/nap-notify-pr-11/) | Open PRPR #11 | DraftDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-NZB](https://napplet.run/protocol/nap-nzb-pr-136/) nzb | [Runtime-mediated NZB downloads](https://napplet.run/protocol/nap-nzb-pr-136/) | Open PRPR #136 | Not listedDocument: draft | Not shipped | | [NAP-OUTBOX](https://napplet.run/protocol/nap-outbox-pr-32/) outbox | [Outbox-Aware Relay Access](https://napplet.run/protocol/nap-outbox-pr-32/) | Open PRPR #32 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-POW](https://napplet.run/protocol/nap-pow-pr-39/) pow | [NIP-13 Proof-of-Work Miner](https://napplet.run/protocol/nap-pow-pr-39/) | Open PRPR #39 | DraftDocument: draft | Not shipped | | [NAP-RELAY](https://napplet.run/protocol/nap-relay-pr-2/) relay | [Relay Proxy](https://napplet.run/protocol/nap-relay-pr-2/) | Open PRPR #2 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-RESOURCE](https://napplet.run/protocol/nap-resource-pr-80/) resource | [Sandboxed Resource Fetching](https://napplet.run/protocol/nap-resource-pr-80/) | Open PRPR #80 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-SERIAL](https://napplet.run/protocol/nap-serial-pr-61/) serial | [Shell-Mediated Serial Device Access](https://napplet.run/protocol/nap-serial-pr-61/) | Open PRPR #61 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-SOULSEEK](https://napplet.run/protocol/nap-soulseek-pr-135/) soulseek | [Runtime-Mediated Soulseek File Discovery and Downloads](https://napplet.run/protocol/nap-soulseek-pr-135/) | Open PRPR #135 | Not listedDocument: draft | Not shipped | | [NAP-STORAGE](https://napplet.run/protocol/nap-storage-pr-3/) storage | [Scoped Key-Value Storage](https://napplet.run/protocol/nap-storage-pr-3/) | Open PRPR #3 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-TORRENT](https://napplet.run/protocol/nap-torrent-pr-72/) torrent | [Runtime-Mediated Torrents](https://napplet.run/protocol/nap-torrent-pr-72/) | Open PRPR #72 | Not listedDocument: draft | Not shipped | | [NAP-UPLOAD](https://napplet.run/protocol/nap-upload-pr-33/) upload | [Media and Blob Upload](https://napplet.run/protocol/nap-upload-pr-33/) | Open PRPR #33 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-WEBRTC](https://napplet.run/protocol/nap-webrtc-pr-59/) webrtc | [Runtime-Mediated WebRTC Signaling](https://napplet.run/protocol/nap-webrtc-pr-59/) | Open PRPR #59 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-CAPTURE](https://napplet.run/protocol/nap-capture-pr-94/) capture | [Shell-Mediated Microphone Recording](https://napplet.run/protocol/nap-capture-pr-94/) | Draft PRPR #94 | Not listedDocument: draft | Not shipped | | [NAP-CLASS](https://napplet.run/protocol/nap-class-pr-16/) class | [Napplet Class Authority](https://napplet.run/protocol/nap-class-pr-16/) | Draft PRPR #16 | DeferredDocument: draft | Not shipped | | [NAP-CLASS-1](https://napplet.run/protocol/nap-class-1-pr-17/) Domain not declared | [Strict Baseline Class](https://napplet.run/protocol/nap-class-1-pr-17/) | Draft PRPR #17 | DeferredDocument: draft | Not shipped | | [NAP-CLASS-2](https://napplet.run/protocol/nap-class-2-pr-18/) Domain not declared | [User-Approved Explicit-Origin Class](https://napplet.run/protocol/nap-class-2-pr-18/) | Draft PRPR #18 | DeferredDocument: draft | Not shipped | | [NAP-CONFIG](https://napplet.run/protocol/nap-config-pr-14/) config | [Per-Napplet Declarative Configuration](https://napplet.run/protocol/nap-config-pr-14/) | Draft PRPR #14 | DraftDocument: draft | [exploratory](https://napplet.run/docs/naps/maturity.html) | | [NAP-CONNECT](https://napplet.run/protocol/nap-connect-pr-19/) connect | [User-Gated Direct Network Access](https://napplet.run/protocol/nap-connect-pr-19/) | Draft PRPR #19 | DeferredDocument: draft | Not shipped | | [NAP-DISPLAY](https://napplet.run/protocol/nap-display-pr-97/) display | [Runtime-Controlled Pixel Displays](https://napplet.run/protocol/nap-display-pr-97/) | Draft PRPR #97 | DraftDocument: draft | Not shipped | | [NAP-DM](https://napplet.run/protocol/nap-dm-pr-74/) dm | [Direct Messages](https://napplet.run/protocol/nap-dm-pr-74/) | Draft PRPR #74 | Not listedDocument: draft | [experimental](https://napplet.run/docs/naps/maturity.html) | | [NAP-EMAIL](https://napplet.run/protocol/nap-email-pr-139/) email | [Shell-Mediated Email](https://napplet.run/protocol/nap-email-pr-139/) | Draft PRPR #139 | Not listedDocument: draft | Not shipped | | [NAP-HASHTREE](https://napplet.run/protocol/nap-hashtree-pr-73/) hashtree | [Runtime-Mediated Hashtree Document Trees](https://napplet.run/protocol/nap-hashtree-pr-73/) | Draft PRPR #73 | Not listedDocument: draft | Not shipped | | [NAP-LOCATION](https://napplet.run/protocol/nap-location-pr-125/) location | [Shell-Mediated Geolocation](https://napplet.run/protocol/nap-location-pr-125/) | Draft PRPR #125 | Not listedDocument: draft | Not shipped | | [NAP-MEDIA](https://napplet.run/protocol/nap-media-pr-133/) media | [Media Session Control](https://napplet.run/protocol/nap-media-pr-133/) | Draft PRPR #133 | DraftDocument: draft | [supported](https://napplet.run/docs/naps/maturity.html) | | [NAP-MESH](https://napplet.run/protocol/nap-mesh-pr-122/) mesh | [Hop-Limited Circle Publish and Subscribe](https://napplet.run/protocol/nap-mesh-pr-122/) | Draft PRPR #122 | Not listedDocument: draft | Not shipped | | [NAP-MOTION](https://napplet.run/protocol/nap-motion-pr-124/) motion | [Shell-Mediated Device Motion and Orientation](https://napplet.run/protocol/nap-motion-pr-124/) | Draft PRPR #124 | Not listedDocument: draft | Not shipped | | [NAP-SYSTEM](https://napplet.run/protocol/nap-system-pr-64/) system | [Read-Only Runtime System Information](https://napplet.run/protocol/nap-system-pr-64/) | Draft PRPR #64 | Not listedDocument: draft | Not shipped | | [NAP-VALUE](https://napplet.run/protocol/nap-value-pr-30/) value | [Value Transfer](https://napplet.run/protocol/nap-value-pr-30/) | Draft PRPR #30 | DraftDocument: draft | Not shipped | | [NAP-WORKER](https://napplet.run/protocol/nap-worker-pr-107/) worker | [Runtime-owned Workers](https://napplet.run/protocol/nap-worker-pr-107/) | Draft PRPR #107 | Not listedDocument: draft | Not shipped | Have a missing capability in mind? ## Start with the boundary. A runtime API, a shared message shape, and a napplet role belong in different places. [Find the right proposal →](https://napplet.run/protocol/contribute/) --- [← All NAPs](https://napplet.run/protocol/) NAP-BLE / PR #62 # Runtime-Mediated Bluetooth Low Energy Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** GATT binding has unit coverage; real-device behavior across host backends is not established by these tests. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/62), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/b46b86913c23cd509d2ace14f1de55b0c451e0f9/naps/NAP-BLE.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/62/files). ## NAP-BLE ### Runtime-Mediated Bluetooth Low Energy `draft` **NAP ID:** NAP-BLE **Domain:** `ble` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.ble`; domain presence signals availability. ### Description NAP-BLE lets a napplet communicate with user-approved Bluetooth Low Energy devices through GATT. The runtime owns discovery, chooser UI, permission, connection lifecycle, notification setup, disconnect handling, and backend policy. Napplets receive only opaque sessions and GATT attribute operations. The contract is projection-neutral. Web runtimes MAY use Web Bluetooth; native runtimes MAY use Core Bluetooth, BlueZ, Android Bluetooth APIs, or another BLE backend. Backend handles never cross the seam. NAP-BLE does not define device protocols. Byte layout, framing, checksums, and commands belong to the napplet or a higher-level protocol. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `open` | `request` (`BleOpenRequest`) | `BleOpenResult` | `ble.open` / `.result` | | `services` | `sessionId` | list of `BleService` | `ble.services` / `.result` | | `read` | `sessionId`, `target` | `bstr` | `ble.read` / `.result` | | `write` | `sessionId`, `target`, `data`, `opts?` | none | `ble.write` / `.result` | | `subscribe` | `sessionId`, `target` | none | `ble.subscribe` / `.result` | | `unsubscribe` | `sessionId`, `target` | none | `ble.unsubscribe` / `.result` | | `close` | `sessionId`, `reason?` | none | `ble.close` / `.result` | | `onEvent` | handler for `BleEvent` | `Subscription` | `ble.event` | #### Schemas Primitive aliases and enums: | Name | Definition | | --- | --- | | `BleUuid` | text or unsigned integer UUID reference | | `BleSessionState` | `opening`, `open`, `closed` | | `BleWriteResponse` | `with-response`, `without-response`, `auto` | `BleOpenRequest` fields: | Field | Required | Type | | --- | --- | --- | | `filters` | no | list of `BleDeviceFilter` | | `exclusionFilters` | no | list of `BleDeviceFilter` | | `acceptAllDevices` | no | boolean | | `optionalServices` | no | list of `BleUuid` | | `label` | no | text | `BleDeviceFilter` fields: | Field | Required | Type | | --- | --- | --- | | `services` | no | list of `BleUuid` | | `name` | no | text | | `namePrefix` | no | text | | `manufacturerData` | no | list of `BleManufacturerDataFilter` | | `serviceData` | no | list of `BleServiceDataFilter` | `BleManufacturerDataFilter` fields: | Field | Required | Type | | --- | --- | --- | | `companyIdentifier` | yes | unsigned integer | | `dataPrefix` | no | bytes | | `mask` | no | bytes | `BleServiceDataFilter` fields: | Field | Required | Type | | --- | --- | --- | | `service` | yes | `BleUuid` | | `dataPrefix` | no | bytes | | `mask` | no | bytes | `BleOpenResult` fields: | Field | Required | Type | | --- | --- | --- | | `session` | yes | `BleSession` | `BleSession` fields: | Field | Required | Type | | --- | --- | --- | | `id` | yes | text | | `state` | yes | `BleSessionState` | | `device` | yes | `BleDeviceInfo` | `BleDeviceInfo` fields: | Field | Required | Type | | --- | --- | --- | | `id` | yes | runtime-scoped opaque text | | `name` | no | text | | `services` | no | list of text | `BleService` fields: | Field | Required | Type | | --- | --- | --- | | `uuid` | yes | text | | `characteristics` | yes | list of `BleCharacteristic` | `BleCharacteristic` fields: | Field | Required | Type | | --- | --- | --- | | `uuid` | yes | text | | `properties` | yes | `BleCharacteristicProperties` | `BleCharacteristicProperties` fields: | Field | Required | Type | | --- | --- | --- | | `read` | no | boolean | | `write` | no | boolean | | `writeWithoutResponse` | no | boolean | | `notify` | no | boolean | | `indicate` | no | boolean | `BleAttribute` fields: | Field | Required | Type | | --- | --- | --- | | `service` | yes | `BleUuid` | | `characteristic` | yes | `BleUuid` | | `descriptor` | no | `BleUuid` | `BleWriteOptions` fields: | Field | Required | Type | | --- | --- | --- | | `response` | no | `BleWriteResponse` | `BleEvent` variants: | Variant | Required fields | Optional fields | | --- | --- | --- | | state | `type: "state"`, `sessionId: tstr`, `state: BleSessionState` | none | | notification | `type: "notification"`, `sessionId: tstr`, `target: BleAttribute`, `data: bstr` | none | | closed | `type: "closed"`, `sessionId: tstr` | `reason: tstr` | `BleUuid` requests MAY use assigned names, 16-bit or 32-bit numbers, or canonical UUID strings. Results MUST use canonical lowercase UUID strings. Byte arrays are integers in `0..255`. ### Operation Rules | Operation | Rules | | --- | --- | | `open` | Runtime selects, authorizes, and connects. Request MUST include at least one filter or `acceptAllDevices: true`. `acceptAllDevices` excludes `filters` and `exclusionFilters`. | | `services` | Returns granted services and characteristics. Runtime MAY redact outside the grant. | | `read` | Reads a characteristic or descriptor value. | | `write` | Writes bytes to a characteristic or descriptor. `response` selects with-response, without-response, or runtime auto. | | `subscribe` | Starts characteristic notifications or indications. `target.descriptor` MUST be omitted. | | `unsubscribe` | Stops characteristic notifications or indications. | | `close` | Disconnects the session. Runtime MAY also close on device disconnect, policy change, or napplet unload. | | `onEvent` | Receives runtime-pushed state, notification, and close events. | ### Projection Backends - Web runtimes MAY implement `ble` with Web Bluetooth. - Native runtimes MAY implement `ble` with Core Bluetooth, BlueZ, Android Bluetooth APIs, platform plugins, or another native BLE backend. - Runtimes MUST NOT expose backend-specific handles to napplets. - Chooser UX, remembered devices, adapter selection, permission prompts, and OS device identifiers stay runtime-owned. ### Wire Protocol `ble.*` messages use [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format: `{ "type": "domain.action", ...payload }`. | Type | Direction | Payload fields | | --- | --- | --- | | `ble.open` | napplet -> runtime | `id`, `request` | | `ble.open.result` | runtime -> napplet | `id`, `session?`, `error?` | | `ble.services` | napplet -> runtime | `id`, `sessionId` | | `ble.services.result` | runtime -> napplet | `id`, `services?`, `error?` | | `ble.read` | napplet -> runtime | `id`, `sessionId`, `target` | | `ble.read.result` | runtime -> napplet | `id`, `data?`, `error?` | | `ble.write` | napplet -> runtime | `id`, `sessionId`, `target`, `data`, `options?` | | `ble.write.result` | runtime -> napplet | `id`, `error?` | | `ble.subscribe` | napplet -> runtime | `id`, `sessionId`, `target` | | `ble.subscribe.result` | runtime -> napplet | `id`, `error?` | | `ble.unsubscribe` | napplet -> runtime | `id`, `sessionId`, `target` | | `ble.unsubscribe.result` | runtime -> napplet | `id`, `error?` | | `ble.close` | napplet -> runtime | `id`, `sessionId`, `reason?` | | `ble.close.result` | runtime -> napplet | `id`, `error?` | | `ble.event` | runtime -> napplet | `event` | `ble.event` has no `id`; it is runtime-pushed. The runtime MUST NOT expose raw `BluetoothDevice`, GATT objects, `navigator.bluetooth`, native handles, adapter addresses, stable device addresses, or OS device paths. ### Error Handling Result messages include `error` when the runtime cannot fulfill the request. Common errors: `"unsupported"`, `"policy denied"`, `"user cancelled"`, `"permission denied"`, `"device unavailable"`, `"open failed"`, `"session not found"`, `"service not found"`, `"characteristic not found"`, `"descriptor not found"`, `"operation not supported"`, `"invalid data"`, `"read failed"`, `"write failed"`, `"device disconnected"`. `ble.open.result` MUST NOT include `session` when it includes `error`. ### Runtime Behavior - MUST own all Bluetooth backend calls. - MUST answer every request with the same `id`. - MUST obtain user approval and satisfy backend activation or permission gates before opening a session. - MUST scope sessions to the requesting napplet and close them on unload or permission loss. - MUST NOT allow access outside the granted device scope. - MUST deliver notifications through `ble.event`. - MUST NOT expose raw Bluetooth objects, native handles, stable device addresses, adapter addresses, or OS device paths. - MAY remember devices, enforce allowlists, limit payload size, limit concurrent sessions, redact metadata, or require a user gesture for `open()`. ### Security Considerations BLE devices may control hardware, leak local data, or expose stable identifiers. The runtime is the policy boundary. - Require explicit user or policy approval before opening a session. - Show the requesting napplet and optional `label` when helpful. - Prefer narrow filters unless policy permits `acceptAllDevices`. - Expose minimum device metadata. - Treat incoming bytes as untrusted and apply rate, size, and lifecycle limits. ### References - Web Bluetooth Community Group specification: [https://webbluetoothcg.github.io/web-bluetooth/](https://webbluetoothcg.github.io/web-bluetooth/) - MDN `Bluetooth.requestDevice()` reference: [https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice) ### Implementations - None yet. ### Changelog - `5f44ca1` - Introduced NAP-BLE as a portable BLE GATT access surface. - `0212e80` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #62 ↗](https://github.com/napplet/naps/pull/62)[Read proposed changes ↗](https://github.com/napplet/naps/pull/62/files) On this pageSpecification metadata Capability domain `ble` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `b46b86913c23` [View source on GitHub ↗](https://github.com/napplet/naps/pull/62/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-BLOSSOM / PR #71 # Low-Level Blossom Blob Access Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/5e74daa4808290a3efb8de117502d52ba2e0d2a5/naps/NAP-BLOSSOM.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/71/files). ## NAP-BLOSSOM ### Low-Level Blossom Blob Access `draft` **NAP ID:** NAP-BLOSSOM **Domain:** `blossom` **Depends:** - `identity` -- capability · optional -- authenticated Blossom requests and BUD-03 server-list mutations use the current shell-user identity. - `relay` -- capability · optional -- BUD-03 server-list helpers read and publish kind `10063` events through the runtime relay surface. - `relay` -- wire · optional -- imports `RelayEventResult` for BUD-03 server-list event returns. - `resource` -- wire · optional -- imported `RelayEventResult.sidecar.resources?` carries `ResourceSidecarEntry[]`, a type owned by the `resource` domain. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.blossom`; domain presence signals availability. ### Description NAP-BLOSSOM gives napplets a low-level, shell-mediated interface to [Blossom](https://github.com/hzrd149/blossom) blob servers. A napplet names the server, blob hash, bytes, and endpoint intent. The shell performs the HTTP request, applies runtime network policy, constructs and signs any required Blossom authorization token, and returns endpoint-shaped results. This is not a generic network escape hatch. It only covers Blossom server endpoints and Blossom URI helpers. NAP-RESOURCE remains the generic byte-fetch surface. NAP-UPLOAD remains the high-level "store these bytes somewhere" surface where the shell chooses NIP-96, Blossom, or another rail. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `check` | `request` (`BlossomBlobCheckRequest`) | `BlossomBlobCheckResult` | `blossom.check` / `blossom.check.result` | | `get` | `request` (`BlossomGetRequest`) | `BlossomBytesResult` | `blossom.get` / `blossom.get.result` | | `checkUpload` | `request` (`BlossomCheckRequest`) | `BlossomCheckResult` | `blossom.checkUpload` / `blossom.checkUpload.result` | | `upload` | `request` (`BlossomUploadRequest`) | `BlossomDescriptorResult` | `blossom.upload` / `blossom.upload.result` | | `mirror` | `request` (`BlossomMirrorRequest`) | `BlossomDescriptorResult` | `blossom.mirror` / `blossom.mirror.result` | | `checkMedia` | `request` (`BlossomCheckRequest`) | `BlossomCheckResult` | `blossom.checkMedia` / `blossom.checkMedia.result` | | `media` | `request` (`BlossomMediaRequest`) | `BlossomDescriptorResult` | `blossom.media` / `blossom.media.result` | | `list` | `request` (`BlossomListRequest`) | `BlossomListResult` | `blossom.list` / `blossom.list.result` | | `delete` | `request` (`BlossomDeleteRequest`) | `BlossomDeleteResult` | `blossom.delete` / `blossom.delete.result` | | `servers` | none | `BlossomServerListResult` | `blossom.servers` / `blossom.servers.result` | | `addServer` | `request` (`BlossomAddServerRequest`) | `BlossomServerListResult` | `blossom.addServer` / `blossom.addServer.result` | | `removeServer` | `server` (`ServerUrl`) | `BlossomServerListResult` | `blossom.removeServer` / `blossom.removeServer.result` | | `reorderServers` | `servers` (list of `ServerUrl`) | `BlossomServerListResult` | `blossom.reorderServers` / `blossom.reorderServers.result` | | `parseUri` | `uri` (`tstr`) | `BlossomUri` | local helper; no wire message | | `formatUri` | `uri` (`BlossomUri`) | `tstr` | local helper; no wire message | #### Schemas `RelayEventResult` is owned by NAP-RELAY. NAP-BLOSSOM imports it by name for kind `10063` server-list events returned by the server-list helpers. Primitive aliases and enums: | Name | Definition | | --- | --- | | `HexSha256` | SHA-256 digest encoded as lowercase hex | | `HexPubkey` | 32-byte public key encoded as lowercase hex | | `ServerUrl` | Blossom server URL | | `BlobExtension` | blob filename extension text | | `MimeType` | MIME type text | | `ErrorCode` | machine-readable error text | | `NostrTag` | list of text tag fields | | `RelayEventResult` | external type owned by NAP-RELAY | | `BlossomAuthMode` | `auto`, `required`, `none` | Request records: | Record | Required fields | Optional fields | | --- | --- | --- | | `BlossomBlobCheckRequest` | `server: ServerUrl`, `sha256: HexSha256` | `extension: BlobExtension`, `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomGetRequest` | `server: ServerUrl`, `sha256: HexSha256` | `extension: BlobExtension`, `range: tstr`, `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomCheckRequest` | `server: ServerUrl`, `sha256: HexSha256`, `size: uint`, `type: MimeType` | `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomUploadRequest` | `server: ServerUrl`, `data: bstr` | `sha256: HexSha256`, `type: MimeType`, `size: uint`, `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomMirrorRequest` | `server: ServerUrl`, `url: tstr` | `sha256: HexSha256`, `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomMediaRequest` | `server: ServerUrl`, `data: bstr` | `sha256: HexSha256`, `type: MimeType`, `size: uint`, `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomListRequest` | `server: ServerUrl`, `pubkey: HexPubkey` | `cursor: HexSha256`, `limit: uint`, `auth: BlossomAuthMode`, `purpose: tstr` | | `BlossomDeleteRequest` | `server: ServerUrl`, `sha256: HexSha256` | `extension: BlobExtension`, `auth: BlossomAuthMode`, `purpose: tstr` | When `auth` is omitted, shells use `auto`. `purpose` is shell-owned diagnostic prompt text only. `BlossomHeaders` fields: | Field | Required | Type | | --- | --- | --- | | `contentType` | no | `MimeType` | | `contentLength` | no | unsigned integer | | `acceptRanges` | no | text | | `sunset` | no | text | | `reason` | no | text | `BlossomDescriptor` fields: | Field | Required | Type | | --- | --- | --- | | `url` | yes | text | | `sha256` | yes | `HexSha256` | | `size` | yes | unsigned integer | | `type` | yes | `MimeType` | | `uploaded` | yes | unsigned integer | | `nip94` | no | list of `NostrTag` | | additional fields | no | any | Result records: | Record | Required fields | Optional fields | | --- | --- | --- | | `BlossomBlobCheckResult` | `ok: bool`, `status: uint`, `headers: BlossomHeaders` | `error: ErrorCode` | | `BlossomBytesResult` | `ok: bool`, `status: uint` | `blob: bstr`, `headers: BlossomHeaders`, `error: ErrorCode` | | `BlossomCheckResult` | `ok: bool`, `status: uint` | `headers: BlossomHeaders`, `error: ErrorCode` | | `BlossomDescriptorResult` | `ok: bool`, `status: uint` | `descriptor: BlossomDescriptor`, `error: ErrorCode` | | `BlossomListResult` | `ok: bool`, `status: uint`, `blobs: list of BlossomDescriptor` | `cursor: HexSha256`, `error: ErrorCode` | | `BlossomDeleteResult` | `ok: bool`, `status: uint` | `error: ErrorCode` | `BlossomAddServerRequest` fields: | Field | Required | Type | | --- | --- | --- | | `server` | yes | `ServerUrl` | | `position` | no | unsigned integer | `BlossomServerListResult` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | | `servers` | yes | list of `ServerUrl` | | `eventId` | no | text | | `result` | no | `RelayEventResult` | | `error` | no | `ErrorCode` | `BlossomUri` fields: | Field | Required | Type | | --- | --- | --- | | `sha256` | yes | `HexSha256` | | `extension` | yes | `BlobExtension` | | `servers` | yes | list of `ServerUrl` | | `authors` | yes | list of `HexPubkey` | | `size` | no | unsigned integer | **`check(request)`** -- Checks whether `server` has the blob identified by `sha256`, using the BUD-01 `HEAD /` endpoint under the hood. Returns status and safe metadata headers. It never returns bytes. **`get(request)`** -- Performs BUD-01 `GET /`. The shell MUST verify that returned bytes hash to `request.sha256` before delivering `blob`. When `range` is provided, the result MAY be a partial byte range; the shell MUST NOT claim whole-blob verification for partial content. **`checkUpload(request)`** -- Performs BUD-06 `HEAD /upload` using `X-SHA-256`, `X-Content-Type`, and `X-Content-Length`. The result is advisory. A later `upload` can still fail. **`upload(request)`** -- Performs BUD-02 `PUT /upload`. The shell sends the exact supplied bytes. It MUST NOT transform them. When `sha256` is supplied, the shell MUST verify it against `data` before sending and use it for preflight or authorization scoping. **`mirror(request)`** -- Performs BUD-04 `PUT /mirror`. The shell submits the remote URL to the target server. When `sha256` is supplied, the shell MUST scope authorization to that hash and reject a returned descriptor with a different hash. **`checkMedia(request)`** -- Performs BUD-05 `HEAD /media`. The result is advisory. A later `media` can still fail. **`media(request)`** -- Performs BUD-05 `PUT /media`. The Blossom server may optimize or transform media. The returned descriptor describes the stored result, not necessarily the original bytes. **`list(request)`** -- Performs BUD-12 `GET /list/`. This endpoint is optional and unrecommended by Blossom. Shells MUST expose server failure as a normal result, not as proof that the user has no blobs. **`delete(request)`** -- Performs BUD-12 `DELETE /`. A delete request acts on exactly one hash. Multiple authorization `x` tags, if the shell creates them, MUST NOT be treated as a multi-delete request. **`servers()`** -- Returns the shell-user's ordered BUD-03 server list from the latest kind `10063` event. When the source event is returned, it is returned as `RelayEventResult`. Returns an empty list when no list exists. This is a Blossom-specific helper for the user's preferred upload/retrieval servers, not a generic list editor. **`addServer(request)`** -- Adds `server` to the shell-user's BUD-03 list and publishes a replacement kind `10063` event. If `server` already exists, the shell MAY treat the request as idempotent or move it to `position` when supplied. The server URL MUST include `http://` or `https://`, matching BUD-03. **`removeServer(server)`** -- Removes `server` from the shell-user's BUD-03 list and publishes the replacement kind `10063` event. Removing an absent server is idempotent. Removing the final server MUST be rejected with `empty-server-list`, because BUD-03 events require at least one `server` tag. **`reorderServers(servers)`** -- Replaces the ordering of the shell-user's BUD-03 list. The supplied list MUST contain the same server set as the current list; callers use `addServer` or `removeServer` to change membership. Empty lists MUST be rejected with `empty-server-list`. **`parseUri(uri)`** -- Parses a BUD-10 `blossom:` URI into hash, extension, server hints, author hints, and size. It performs no network I/O. **`formatUri(uri)`** -- Produces a BUD-10 `blossom:` URI. It MUST include the hash and extension, defaulting the extension to `.bin` when no better extension is known. ### Wire Protocol `blossom.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `blossom.check` | napplet -> shell | `id`, `request` | | `blossom.check.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.get` | napplet -> shell | `id`, `request` | | `blossom.get.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.checkUpload` | napplet -> shell | `id`, `request` | | `blossom.checkUpload.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.upload` | napplet -> shell | `id`, `request` | | `blossom.upload.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.mirror` | napplet -> shell | `id`, `request` | | `blossom.mirror.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.checkMedia` | napplet -> shell | `id`, `request` | | `blossom.checkMedia.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.media` | napplet -> shell | `id`, `request` | | `blossom.media.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.list` | napplet -> shell | `id`, `request` | | `blossom.list.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.delete` | napplet -> shell | `id`, `request` | | `blossom.delete.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.servers` | napplet -> shell | `id` | | `blossom.servers.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.addServer` | napplet -> shell | `id`, `request` | | `blossom.addServer.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.removeServer` | napplet -> shell | `id`, `server` | | `blossom.removeServer.result` | shell -> napplet | `id`, `result`, `error?` | | `blossom.reorderServers` | napplet -> shell | `id`, `servers` | | `blossom.reorderServers.result` | shell -> napplet | `id`, `result`, `error?` | Key design notes: - Request/result pairs use `id` for correlation. - Binary `data` and `blob` fields cross the boundary as projection-native byte objects. In the web projection that means `Blob` or `ArrayBuffer`, not base64-encoded strings. - Result `status` is the upstream HTTP status after shell policy checks. Shell policy failures MAY use `status: 0` with a stable `error`. - `headers.reason` carries BUD `X-Reason` diagnostics only. Napplets MUST NOT branch on it. - BUD-03 server-list helpers are shell-owned read/replace mutations over kind `10063`. Returned server-list events use `RelayEventResult` so resource sidecars and relay hints follow the same shape as other raw event returns. Runtimes MAY implement them using the same internal machinery as a generic list API, but NAP-BLOSSOM exposes the Blossom-specific verbs directly. - Local helpers (`parseUri`, `formatUri`) do not send wire messages. #### Examples **Check whether a server has a blob:** ``` -> { "type": "blossom.check", "id": "c1", "request": { "server": "https://cdn.example.com", "sha256": "b167...", "extension": ".pdf" } } <- { "type": "blossom.check.result", "id": "c1", "result": { "ok": true, "status": 200, "headers": { "contentType": "application/pdf", "contentLength": 184292 } } } ``` **Check, then upload exact bytes:** ``` -> { "type": "blossom.checkUpload", "id": "b1", "request": { "server": "https://cdn.example.com", "sha256": "b167...", "size": 184292, "type": "application/pdf" } } <- { "type": "blossom.checkUpload.result", "id": "b1", "result": { "ok": true, "status": 200, "headers": {} } } -> { "type": "blossom.upload", "id": "b2", "request": { "server": "https://cdn.example.com", "data": "", "sha256": "b167...", "type": "application/pdf", "size": 184292 } } <- { "type": "blossom.upload.result", "id": "b2", "result": { "ok": true, "status": 201, "descriptor": { "url": "https://cdn.example.com/b167....pdf", "sha256": "b167...", "size": 184292, "type": "application/pdf", "uploaded": 1725105921 } } } ``` **Add and reorder preferred servers:** ``` -> { "type": "blossom.addServer", "id": "s1", "request": { "server": "https://cdn.example.com", "position": 0 } } <- { "type": "blossom.addServer.result", "id": "s1", "result": { "ok": true, "servers": ["https://cdn.example.com"], "eventId": "ev1..." } } -> { "type": "blossom.addServer", "id": "s2", "request": { "server": "https://cdn2.example.com", "position": 1 } } <- { "type": "blossom.addServer.result", "id": "s2", "result": { "ok": true, "servers": ["https://cdn.example.com", "https://cdn2.example.com"], "eventId": "ev2..." } } -> { "type": "blossom.reorderServers", "id": "s3", "servers": ["https://cdn2.example.com", "https://cdn.example.com"] } <- { "type": "blossom.reorderServers.result", "id": "s3", "result": { "ok": true, "servers": ["https://cdn2.example.com", "https://cdn.example.com"], "eventId": "ev3..." } } ``` **Retrieve by hash:** ``` -> { "type": "blossom.get", "id": "b3", "request": { "server": "https://cdn.example.com", "sha256": "b167...", "extension": ".pdf" } } <- { "type": "blossom.get.result", "id": "b3", "result": { "ok": true, "status": 200, "blob": "", "headers": { "contentType": "application/pdf", "contentLength": 184292 } } } ``` #### Error Handling All result messages use `result.ok: false` plus `result.error` for programmatic failure. The top-level `error?` field is reserved for malformed envelopes where no typed result can be constructed. Common error codes: `invalid-request`, `invalid-server`, `invalid-hash`, `not-signed-in`, `user-denied`, `blocked-by-policy`, `not-found`, `auth-required`, `payment-required`, `too-large`, `unsupported-media-type`, `hash-mismatch`, `server-list-unavailable`, `invalid-server-list`, `empty-server-list`, `network-error`, `timeout`, `server-error`, `unsupported`. ### Shell Behavior - The shell MUST treat `server`, `url`, hashes, MIME strings, URI hints, and descriptors as untrusted. - The shell MUST respond to every request with a result message carrying the same `id`. - The shell MUST restrict requests to Blossom endpoint shapes. It MUST NOT turn this interface into arbitrary HTTP method, path, or header access. - The shell MUST apply its normal network policy before connecting to any Blossom server or mirror origin. - The shell MUST perform all BUD-11 authorization token construction, signing, encoding, and header attachment. It MUST NOT expose authorization tokens or signing keys to the napplet. - The shell MUST scope authorization tokens with `server` tags derived from the lowercase Blossom server domain, not from a full URL. - The shell MUST scope authorization tokens with `x` tags whenever the target endpoint implies a hash. - The shell MUST reject `auth: "required"` requests and server-authenticated operations with `not-signed-in` when no shell-user signer is connected. - The shell MUST publish BUD-03 server-list mutations as kind `10063` events with empty `content` and ordered `server` tags. - The shell MUST reject BUD-03 server-list mutation requests that would publish zero `server` tags. - The shell MUST preserve the caller-visible BUD-03 server ordering exactly in `servers`, `addServer`, `removeServer`, and `reorderServers` results. - The shell SHOULD merge observed BUD-03 relay URLs into `RelayEventResult.sidecar.relayHints` when it can disclose them. - The shell MUST reject server-list mutation requests with `not-signed-in` when no shell-user signer is connected. - The shell MAY prompt, deny, rate-limit, cache, redact diagnostics, or route by napplet policy. - The shell MAY try unsigned requests for public endpoints when `auth` is `"auto"` and no shell-user signer is connected. - The shell MAY send unsigned requests when `auth: "none"` is supplied, except when runtime policy requires user consent. ### Security Considerations - Blossom server access is a network boundary. Shells MUST enforce SSRF protections, redirect limits, size limits, and per-napplet rate limits before delivering results. - BUD-11 tokens are bearer credentials for their lifetime. Shells SHOULD keep expirations short, scope tokens to exact server domains, and scope hash-based operations with `x` tags. - `mirror` asks a server to fetch a remote URL. Shells SHOULD apply policy to the origin URL before forwarding it and SHOULD make the destination server visible in consent UI. - `media` may transform bytes. Napplets that require exact byte preservation MUST use `upload`, not `media`. - `list` can reveal a user's stored blobs. Shells SHOULD require explicit consent when listing a pubkey that belongs to the shell-user. - BUD-03 server lists are user preference and routing state. Shells SHOULD show the requesting napplet, server URL, and resulting order before publishing a mutation. - Successful upload, mirror, delete, or media results prove only what the Blossom server reported. They do not prove future availability or deletion across other servers. ### References - [Blossom](https://github.com/hzrd149/blossom) - [BUD-01: Server requirements and blob retrieval](https://github.com/hzrd149/blossom/blob/master/buds/01.md) - [BUD-02: Blob upload](https://github.com/hzrd149/blossom/blob/master/buds/02.md) - [BUD-03: User Server List](https://github.com/hzrd149/blossom/blob/master/buds/03.md) - [BUD-04: Mirroring blobs](https://github.com/hzrd149/blossom/blob/master/buds/04.md) - [BUD-05: Media optimization endpoints](https://github.com/hzrd149/blossom/blob/master/buds/05.md) - [BUD-06: Upload requirements](https://github.com/hzrd149/blossom/blob/master/buds/06.md) - [BUD-10: Blossom URI Schema](https://github.com/hzrd149/blossom/blob/master/buds/10.md) - [BUD-11: Nostr Authorization](https://github.com/hzrd149/blossom/blob/master/buds/11.md) - [BUD-12: Blob management endpoints](https://github.com/hzrd149/blossom/blob/master/buds/12.md) ### Implementations - (none yet) ### Changelog - `03ea0db` - Introduced NAP-BLOSSOM as a constrained runtime capability for Blossom blob access. - `b6c1737` - Added runtime-owned Blossom server-list operations and ownership rules. - `d48052f` - Changed Blossom server-list events to use relay-owned result shape and declared the relay dependency. - `e92fb0b` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #71 ↗](https://github.com/napplet/naps/pull/71)[Read proposed changes ↗](https://github.com/napplet/naps/pull/71/files) On this pageSpecification metadata Capability domain `blossom` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies - [identity](https://napplet.run/protocol/nap-identity/) - [relay](https://napplet.run/protocol/nap-relay-pr-2/) - [resource](https://napplet.run/protocol/nap-resource-pr-80/) Source revision `5e74daa48082` [View source on GitHub ↗](https://github.com/napplet/naps/pull/71/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CAPTURE / PR #94 # Shell-Mediated Microphone Recording Draft PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/Datawav/naps/blob/438b37612cd3c54763d791ef85164059a689723e/naps/NAP-CAPTURE.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/94/files). ## NAP-CAPTURE ### Shell-Mediated Microphone Recording `draft` **NAP ID:** NAP-CAPTURE **Domain:** `capture` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.capture`; domain presence signals availability. ### Description NAP-CAPTURE lets a napplet ask its runtime to create a finite, user-approved microphone recording. The runtime owns consent, device selection, platform permission, capture, encoding, limits, indication, retention, and teardown. The napplet receives encoded audio bytes, not microphone authority. It never receives a device handle, stable device identifier, raw stream, recorder object, or platform permission state. NAP-CAPTURE has no dependency on another NAP. Passing a completed artifact to `upload`, `storage`, `relay`, or another domain is a separate napplet action. ### Discovery In the web projection, a napplet detects the capability by checking whether `window.napplet.capture` is present. No shell handshake is required. A runtime that reports support MUST implement every baseline operation in this document. `info()` reports output formats and finite policy limits. It MUST NOT reveal stable device identifiers, device labels, device counts, or platform permission state. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `info` | — | `CaptureInfo` | `capture.info` → `capture.info.result` | | `start` | `CaptureRequest` | `CaptureSession` | `capture.start` → `capture.start.result` | | `status` | `captureId` | `CaptureStatus` | `capture.status` → `capture.status.result` | | `stop` | `captureId` | `CaptureArtifact` | `capture.stop` → `capture.stop.result` | | `cancel` | `captureId` | `CaptureStatus` | `capture.cancel` → `capture.cancel.result` | | `release` | `captureId` | `captureId` | `capture.release` → `capture.release.result` | `status` is authoritative. `capture.changed` is advisory; a missed event never loses a terminal artifact or terminal error before its disclosed expiry. #### `CaptureInfo` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `sources` | yes | list of text | MUST contain only `"microphone"` in this version. | | `mimeTypes` | yes | list of text | MIME types the runtime is prepared to attempt, in preference order. | | `maxDurationMs` | yes | positive integer | Maximum interval before the runtime initiates terminalization. | | `maxBytes` | yes | positive integer | Hard maximum deliverable artifact size. | | `retentionMs` | yes | positive integer | Time a terminal status remains recoverable unless explicitly erased. | `CaptureInfo` is advisory. Policy, device availability, encoder startup, and platform permission can still make a later `start` fail. All three limits MUST be finite; a runtime MUST NOT advertise `null` or an unbounded value. #### `CaptureRequest` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `source` | yes | text | MUST be `"microphone"`. | | `mimeTypes` | no | list of text | Acceptable output MIME types in caller preference order. | An omitted or empty `mimeTypes` list lets the runtime choose any advertised format. Each supplied item MUST be a valid MIME media type or `start` fails with `invalid-request` before consent UI. MIME compatibility is evaluated by parsing media types. Type, subtype, and parameter names are ASCII case-insensitive. Duplicate parameter names make a request item invalid. Parsing removes optional whitespace outside a parameter value. Quoted-string escaping is removed, but whitespace inside the quoted value is preserved. Parameter values are otherwise compared byte-for-byte and case-sensitively. For the `codecs` parameter, split the unquoted comma-separated value and remove optional whitespace around each token. Compare the resulting ordered token list case-sensitively by default. A registered codec namespace MAY define a different comparison rule; apply it only when both tokens belong to that namespace. Every parameter requested by the caller MUST be present and equal under this algorithm in the actual output; additional actual parameters are allowed. Parameter order does not affect compatibility. The runtime MUST apply this same algorithm wherever this document says MIME-compatible. The runtime MUST choose an advertised format compatible with at least one requested item or fail with `unsupported-format` before consent UI. The returned `mimeType` is the normalized actual output type, not necessarily the caller's original string. An advertised format is one the runtime is prepared to attempt, not a guarantee that the platform encoder will later start successfully. The request has no device selector or low-level audio constraints. A runtime MAY offer device and quality choices in trusted shell UI. #### `CaptureSession` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `captureId` | yes | text | Opaque identifier bound to the authenticated requesting endpoint generation. | | `source` | yes | text | `"microphone"`. | | `mimeType` | yes | text | Actual output MIME type selected by the runtime. | | `maxDurationMs` | yes | positive integer | Effective terminalization deadline interval for this session. | | `maxBytes` | yes | positive integer | Effective hard artifact-size limit for this session. | | `retentionMs` | yes | positive integer | Effective terminal-status retention interval for this session. | A successful `start` result means consent succeeded and recording is active. A runtime MUST NOT return a `CaptureSession` while consent is pending. #### `CaptureArtifact` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `captureId` | yes | text | Identifier of the completed capture. | | `data` | yes | any | Projection-defined carrier for one immutable, complete encoded audio byte sequence. It is never text, base64, a live stream, or a device/recorder handle. | | `mimeType` | yes | text | Normalized actual MIME type of `data`. | | `size` | yes | non-negative integer | Exact byte length of `data`. | | `durationMs` | yes | non-negative integer | Runtime-measured active capture interval. | | `truncated` | yes | boolean | `true` when a limit or interruption ended capture. | | `reason` | yes | `"requested"`, `"user"`, `"limit"`, or `"interrupted"` | Terminal cause. | `durationMs` is monotonic elapsed time from recorder activation to the terminal decision, excluding consent and backend finalization time; it is not parsed container duration. `maxDurationMs` requires the runtime to initiate the terminal decision by that deadline, but scheduler and backend latency MAY make reported `durationMs` larger. `maxBytes` is a hard upper bound on a deliverable artifact. `size` MUST equal the binary byte length and MUST NOT exceed the session's `maxBytes`. The runtime MUST report the actual container and codec in `mimeType`; it MUST NOT merely echo the request. #### `CaptureStatus` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `captureId` | yes | text | Affected capture. | | `state` | yes | `"recording"`, `"completed"`, `"cancelled"`, or `"failed"` | Current observable state. | | `artifact` | completed only | `CaptureArtifact` | Retained terminal artifact. | | `reason` | cancelled only | `"requested"`, `"user"`, or `"revoked"` | Stable cancellation reason. | | `error` | failed only | `CaptureError` | Retained normalized failure. | | `expiresAt` | terminal only | non-negative integer | Unix time in milliseconds when retained terminal state is automatically erased. | A `recording` status MUST omit `artifact`, `reason`, `error`, and `expiresAt`. A `completed` status MUST contain only `artifact` and `expiresAt` among those conditional fields; `cancelled` MUST contain only `reason` and `expiresAt`; and `failed` MUST contain only `error` and `expiresAt`. A terminal `CaptureStatus` remains available until `release`, trusted discard, napplet unload, identity revocation, or `expiresAt`, whichever comes first. The runtime MUST set `expiresAt` to the terminal commit time plus the session's `retentionMs` and automatically erase retained runtime state at that time. A new `start` for the identity fails with `busy` while a pending, recording, finalizing, or retained capture occupies its slot. #### `CaptureEvent` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `captureId` | yes | text | Affected capture. | | `state` | yes | `"completed"`, `"cancelled"`, or `"failed"` | New terminal state. | | `reason` | cancelled only | `"requested"`, `"user"`, or `"revoked"` | Stable cancellation reason. | | `error` | failed only | `CaptureError` | Normalized failure. | `capture.changed` carries `CaptureEvent`. It is a wake-up hint only. The napplet uses `status` to retrieve the authoritative terminal state and artifact. There is no subscribe or unsubscribe operation. The runtime automatically pushes `capture.changed` to the bound napplet endpoint while that endpoint generation remains present and authorized. A projection MAY expose an idiomatic event-listener shape as guidance, but that shape is not part of this NAP contract. #### `CaptureError` | Field | Required | Type | Notes | | --- | --- | --- | --- | | `code` | yes | text | Stable error code from the table below. | | `message` | no | text | Safe diagnostic text; MUST NOT expose device identifiers or platform internals. | ### Lifecycle An **endpoint generation** is one authenticated lifetime of a projection endpoint. Each projection MUST define when a generation begins and ends. Each runtime MUST reserve at most one pending, recording, finalizing, or retained capture slot per verified napplet identity. The slot's owner is the authenticated endpoint generation that issued `start`; identity is the scope for attribution, rate limits, and concurrency policy, not authorization to operate another endpoint's capture. The runtime MUST also enforce finite runtime-wide limits on active captures and retained encoded bytes. The identity slot is reserved before trusted consent UI opens. A concurrent `start` for that identity fails with `busy`. Denial, cancellation, or start failure releases a pending reservation. ```text start approved absent --------------------> recording | stop/runtime | cancel decide/finalize| discard v completed / cancelled / failed | release / expiry / teardown v absent ``` #### Terminal decisions A terminal decision selects the intended outcome before asynchronous recorder finalization. The first accepted caller or runtime terminal decision wins, except that trusted **Discard**, permission or policy revocation, identity revocation, and owner-endpoint unload MUST supersede an uncommitted completion decision and erase runtime-held bytes. A runtime MAY use an internal `finalizing` state. While completion is finalizing, `status` reports `recording`; a second `stop`, `cancel`, or `release` fails with `invalid-state`. No artifact or terminal event exists until terminal commit. Terminal commit atomically stores the terminal `CaptureStatus`, makes any completed artifact eligible for delivery, sets `expiresAt`, frees active device resources, and emits the one advisory event. Backend callbacks after commit MUST NOT alter the status, create another artifact, or deliver more bytes. Rules: 1. `start` validates the request and reserves policy capacity before opening trusted UI. It creates a session only after consent succeeds, microphone input is active, and the encoder has started. A pre-session rejection creates no `captureId`, status, or event. 2. `status` returns the observable state. For `completed`, it returns the retained artifact. For `cancelled` or `failed`, it returns the retained reason or error. 3. `stop` on `recording` claims a completion decision and remains pending until finalization commits or fails. On success it retains and returns one artifact with `reason: "requested"`. `stop` on `completed` returns the same retained artifact. `stop` on `cancelled` or `failed` fails with `invalid-state`. 4. `cancel` on `recording` claims a destructive decision, stops input, erases runtime-held bytes, commits `cancelled` with `reason: "requested"`, and returns that status. `cancel` on a terminal capture returns the retained status without altering or erasing it. 5. `release` is valid only for a committed terminal capture. It erases retained runtime status and bytes. Later operations, including a repeated `release`, fail with `capture-not-found`. 6. At `expiresAt`, the runtime performs the same erasure as `release` without a request or event. The identity slot then becomes available. 7. A duration limit requires the runtime to initiate terminalization no later than `maxDurationMs` after activation. A byte limit requires the runtime to stop before or when it determines the next complete valid artifact may exceed policy. If a complete valid artifact is available within `maxBytes`, commit `completed` with `truncated: true` and `reason: "limit"`. The runtime MUST NOT byte-truncate an encoded container. If a complete valid artifact cannot fit, erase it and commit `failed` with `quota-exceeded`. 8. Device loss MAY commit a complete recoverable artifact within `maxBytes` as `completed`, `truncated: true`, `reason: "interrupted"` while recording authority remains valid. Otherwise it commits `failed` with `device-unavailable` and erases bytes. Device loss MUST NOT leave recording active. 9. Permission, policy, or identity revocation stops input and prevents any uncommitted artifact delivery. Permission or policy revocation while the owner endpoint remains valid commits `cancelled` with `reason: "revoked"`; identity revocation erases all matching runtime state without further delivery. 10. Owner-endpoint unload cancels pending consent, stops input, and erases that endpoint generation's runtime state without a result or event. It MUST NOT erase a capture merely because another live endpoint of the same identity unloads, and it MUST NOT continue recording in the background. #### Trusted user controls | Trusted action | Required outcome | | --- | --- | | Decline or dismiss consent before activation | `capture.start.result` with `user-cancelled`; no `captureId`, status, artifact, or event. | | trusted **Stop** during recording | Complete and retain one artifact with `reason: "user"`. | | trusted **Discard** during recording or finalization | Stop input, erase runtime-held bytes, and commit `cancelled` with `reason: "user"`. | | trusted **Discard** after completion | Erase retained runtime state immediately; later operations return `capture-not-found`. | Every terminal commit to `completed`, `cancelled`, or `failed` emits exactly one `capture.changed` event to the owning endpoint generation while it remains present and authorized. Event loss is harmless before expiry because `status` is authoritative. A napplet cannot programmatically cancel consent before `start` returns because no `captureId` exists yet. The runtime MUST cancel pending consent when the owner endpoint unloads; trusted shell UI MUST let the user cancel it directly. Pause/resume is not a baseline v1 operation. ### Wire Protocol All request messages carry a caller-generated correlation `id`. While the source endpoint generation remains present and authorized, each processed request MUST settle exactly once with its matching result type, which echoes that `id`. Endpoint unload or revocation cancels undeliverable pending responses. Request-ID replay behavior is owned by the common transport, not this domain, and callers MUST use a fresh `id` for each operation while an earlier request is pending. The projection authenticates both the source endpoint generation and its verified manifest and artifact identity before dispatch. A `captureId` is generated by the runtime and authorized only for the authenticated endpoint generation that created it. The verified identity remains attribution and policy scope. A request from a foreign identity, a stale/reloaded endpoint generation, or another live endpoint of the same identity MUST fail uniformly with `capture-not-found`. Responses and events MUST be delivered only to the owning endpoint generation. | Type | Direction | Payload fields | | --- | --- | --- | | `capture.info` | napplet → runtime | `id` | | `capture.info.result` | runtime → napplet | `id`, exactly one of `info` or `error` | | `capture.start` | napplet → runtime | `id`, `request` | | `capture.start.result` | runtime → napplet | `id`, exactly one of `session` or `error` | | `capture.status` | napplet → runtime | `id`, `captureId` | | `capture.status.result` | runtime → napplet | `id`, exactly one of `status` or `error` | | `capture.stop` | napplet → runtime | `id`, `captureId` | | `capture.stop.result` | runtime → napplet | `id`, exactly one of `artifact` or `error` | | `capture.cancel` | napplet → runtime | `id`, `captureId` | | `capture.cancel.result` | runtime → napplet | `id`, exactly one of `status` or `error` | | `capture.release` | napplet → runtime | `id`, `captureId` | | `capture.release.result` | runtime → napplet | `id`, exactly one of `captureId` or `error` | | `capture.changed` | runtime → napplet | `event` | #### Result and error routing | Situation | Correlated request outcome | Retained status / event | | --- | --- | --- | | Malformed or unsupported request, policy denial, consent decline, or failure before a session exists | Matching `*.result` with `error` | None | | Unknown, foreign, stale, or wrong-state `captureId` | Matching `*.result` with `error` | No transition and no event | | Successful operation | One matching `*.result` | If and only if it causes the initial terminal commit, retain status and emit one event | | Failure while a requested `stop` is finalizing | `capture.stop.result` with the terminal error | Commit `failed` and emit one event | | Pending `stop` superseded by trusted Discard | `capture.stop.result` with `user-cancelled` | Commit `cancelled` with `reason: "user"` and emit one event | | Pending `stop` superseded by platform-permission or shell-policy revocation | `capture.stop.result` with `permission-denied` or `policy-denied`, respectively | Commit `cancelled` with `reason: "revoked"` and emit one event | | An asynchronous terminal failure after successful `start`, with no operation awaiting completion | No request response | Commit `failed` and emit one event; never emit an uncorrelated result | A pre-session failure and an asynchronous terminal failure are distinct. A `start` remains pending until trusted consent, platform permission, and encoder startup settle. If it fails, only its correlated error is sent. Once a `CaptureSession` exists, a later recorder or device failure is authoritative through retained `status` and `capture.changed`. #### Examples Discover formats and limits: ```json { "type": "capture.info", "id": "cap-info-1" } ``` ```json { "type": "capture.info.result", "id": "cap-info-1", "info": { "sources": ["microphone"], "mimeTypes": ["audio/webm;codecs=opus", "audio/mp4"], "maxDurationMs": 600000, "maxBytes": 52428800, "retentionMs": 300000 } } ``` Start recording: ```json { "type": "capture.start", "id": "cap-start-1", "request": { "source": "microphone", "mimeTypes": ["audio/webm;codecs=opus", "audio/mp4"] } } ``` ```json { "type": "capture.start.result", "id": "cap-start-1", "session": { "captureId": "capture_7f2a", "source": "microphone", "mimeType": "audio/webm;codecs=opus", "maxDurationMs": 600000, "maxBytes": 52428800, "retentionMs": 300000 } } ``` Stop and retain the artifact: ```json { "type": "capture.stop", "id": "cap-stop-1", "captureId": "capture_7f2a" } ``` ```text { "type": "capture.stop.result", "id": "cap-stop-1", "artifact": { "captureId": "capture_7f2a", "data": , "mimeType": "audio/webm;codecs=opus", "size": 18234, "durationMs": 2410, "truncated": false, "reason": "requested" } } ``` Runtime-driven completion emits a hint: ```json { "type": "capture.changed", "event": { "captureId": "capture_7f2a", "state": "completed" } } ``` The napplet then calls `capture.status`; the completed result contains the retained `artifact`. ### Runtime Behavior A conforming runtime: - MUST own microphone access, device selection, encoding, retention, and teardown. - MUST authenticate the source endpoint generation, bind each session and `captureId` to it, and separately retain the verified napplet identity for attribution and policy. - MUST obtain a fresh capture-specific confirmation immediately before every activation. Trusted UI MUST identify the verified requesting napplet using a shell-controlled human label, make its publisher, manifest reference, and verified artifact hash available, name the microphone source, and state that complete encoded audio will be delivered to that napplet and may be copied or sent elsewhere by it. A manifest declaration, prior approval, platform permission grant, or unrelated user gesture MUST NOT substitute for this confirmation. - MUST render trusted shell-controlled indication while microphone capture is active, including the requesting napplet identity and distinct shell-level **Stop** and **Discard** controls with the outcomes defined above. - MUST keep the indicator active while the platform microphone remains active. - MUST normalize platform errors and keep low-level device diagnostics local. - MUST enforce finite duration, artifact-byte, retained-byte, prompt-rate, and concurrency policy independently of caller input. - MUST key prompt-rate policy at least to verified napplet identity so endpoint reload does not reset it. The policy MUST define a finite prompt budget within a positive time window and MUST be checked before opening or refreshing trusted UI; over-budget calls fail with `policy-denied` without showing another capture prompt. - MUST stop capture promptly when the user, platform, shell policy, or owning endpoint lifecycle revokes authority. - MUST erase runtime-held cancelled, released, expired, failed, and unloaded capture bytes and prevent future delivery from those states. - MUST NOT upload, publish, sign, relay, transcribe, or persist beyond this lifecycle as a side effect of capture. The runtime MAY provide trusted device selection, quality controls, or previews in shell UI. Those controls do not alter this contract. ### Errors | Code | Meaning | | --- | --- | | `invalid-request` | The envelope, required field, value type, or MIME syntax is malformed. | | `policy-denied` | Runtime policy rejected the operation, including prompt/capacity denial or revocation. | | `user-cancelled` | Trusted UI declined consent or superseded a pending operation with Discard. | | `permission-denied` | Platform microphone permission was denied or revoked. | | `device-unavailable` | No usable microphone exists, or an active device was lost without a recoverable artifact. | | `unsupported-source` | `source` is not `"microphone"`. | | `unsupported-format` | No advertised output is compatible with an acceptable requested MIME type. | | `busy` | A pending, recording, finalizing, or retained capture reserves the identity slot, or a runtime-wide limit was reached. | | `capture-not-found` | The identifier is unknown, expired, stale, or not owned by the authenticated endpoint generation. | | `invalid-state` | The requested operation is invalid for the observable state or an internal finalization is pending. | | `quota-exceeded` | No complete valid artifact can be retained within byte policy. | | `capture-failed` | Capture or encoding failed for another normalized reason. | A runtime MUST avoid exposing whether a foreign `captureId` exists; use `capture-not-found` for unknown, expired, stale, and foreign identifiers. Request validation MUST occur before consent UI. Failures after a session exists follow the result-and-error routing table and MUST NOT leave input active. ### Security Considerations Microphone access can expose private speech and ambient audio. This contract assumes a conforming trusted runtime: a malicious runtime already holds platform microphone authority and cannot be constrained by napplet messages. Within that trust model, the runtime is the security boundary and MUST mediate capture as a visible, revocable capability. - Platform permission is not napplet consent. A prior platform grant MUST NOT authorize silent capture by a newly loaded napplet. - Consent UI and active-capture indication MUST be rendered by trusted shell UI, not supplied solely by the napplet. - Device identifiers, labels, counts, and permission state MUST NOT cross this API. - Capture identifiers MUST be unguessable and bound to the authenticated owner endpoint generation. Identity equality alone MUST NOT authorize a sibling instance, stale frame, or replacement endpoint. - Runtime limits MUST prevent unbounded recording, retained-byte growth, repeated prompt abuse, and concurrent microphone monopolization. - Completed bytes are sensitive. They MUST be delivered only to the owning endpoint generation. `release`, trusted discard, expiry, unload, and revocation erase runtime-held copies and prevent future runtime delivery; they cannot revoke or erase bytes already delivered to the napplet, copied by it, or sent elsewhere. - Live audio, waveform, and level streaming are excluded. They add continuous listening and high-frequency transport semantics not needed here. ### Projection Each projection MUST define its endpoint-generation lifecycle and the normative carrier and byte-length operation for `CaptureArtifact.data`. The web binding is defined by the [web projection](https://github.com/Datawav/naps/blob/438b37612cd3c54763d791ef85164059a689723e/projections/web.md#capture). ### Non-Goals NAP-CAPTURE v1 does not define: - camera, screen, window, tab, system-audio, or mixed-source capture; - device enumeration, device selection APIs, or persistent device preferences; - raw audio streams, chunks, waveform data, level meters, or WebRTC tracks; - pause/resume; - editing, transcoding, transcription, upload, signing, publishing, or relay use; - durable recovery after napplet unload; - background recording after napplet unload; or - any change to a projection's sandbox policy. ### Implementations None yet. A runtime prototype demonstrating consent, trusted indication, binary delivery, terminal retention, limit handling, and unload cleanup is required before this draft should be considered mergeable. ### Changelog - `8f69d1e` - Define runtime-owned microphone consent, capture lifecycle, retained artifacts, wire messages, limits, and security rules. - `3b83f79` - Define automatic change delivery and a language-neutral release result. - `88b8b49` - Bind captures to endpoint generations and make consent, limits, retention, terminal races, binary delivery, and errors deterministic. - `ad1c293` - Move web carriers to the projection, preserve codec identifier case, use matching result envelopes, and avoid introducing a canonical byte type. - `bddbafd` - Adopted injected-domain discovery and verified manifest/artifact attribution while preserving endpoint-generation ownership. [Join the discussion · PR #94 ↗](https://github.com/napplet/naps/pull/94)[Read proposed changes ↗](https://github.com/napplet/naps/pull/94/files) On this pageSpecification metadata Capability domain `capture` Repository state Draft PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `438b37612cd3` [View source on GitHub ↗](https://github.com/napplet/naps/pull/94/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CATALOG / PR #95 # Runtime Napplet Catalog Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/1020ae53884a177b7ffc58a679126bc8800d0c6d/naps/NAP-CATALOG.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/95/files). ## NAP-CATALOG ### Runtime Napplet Catalog `draft` **NAP ID:** NAP-CATALOG **Domain:** `catalog` **Depends:** - `intent` — layering · optional — when implemented, the runtime shares catalog identifiers and current-handler state with its intent resolver. Catalog queries do not invoke or require the intent capability. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.catalog`; domain presence signals availability. ### Description NAP-CATALOG provides read-only access to the napplets a runtime can currently resolve and load. A napplet receives verified napplet identities, display metadata, advertised archetypes, accepted convention intents and their parameters, and the runtime's current handler for each archetype. The catalog describes availability, not lifecycle. A catalog entry MAY describe a napplet that is not running. A current handler is the napplet the runtime would select for an implicit archetype dispatch at the time of the request. It does not mean that the napplet is running, focused, or visible. NAP-CATALOG does not install, launch, focus, or dispatch to napplets. It does not set handler preferences. It does not define convention semantics. NAP-INTENT owns archetype dispatch. Conventions own their message semantics. See [NAP-INTENT](https://github.com/napplet/naps/blob/1020ae53884a177b7ffc58a679126bc8800d0c6d/naps/NAP-INTENT.md). ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `get` | none | `CatalogSnapshot` | `catalog.get` / `catalog.get.result` | #### Schemas `NappletIdentity` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `kind` | yes | integer | Verified manifest kind: 5129, 15129, or 35129. | | `pubkey` | yes | text | Manifest publisher's 64-character lowercase-hex public key. | | `eventId` | yes | text | Verified manifest event id, 64-character lowercase hex. | | `dTag` | conditional | text | Manifest `d` value; present only for kind 35129. | | `artifactHash` | yes | text | Verified artifact `x` hash, 64-character lowercase hex. | `IntentDescriptor` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `intent` | yes | text | Intent name, such as `open`. | | `convention` | yes | text | Stable queryless convention identity, such as `napplet:note/open`. | | `parameters` | yes | list of text | Parameter names advertised in the manifest i tag, in order. Empty when none are advertised. | `ArchetypeDescriptor` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `archetype` | yes | text | NAAT role slug, such as `note`. | | `intents` | yes | list of `IntentDescriptor` | Accepted intents for this role. | `NappletDescriptor` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Opaque runtime-assigned napplet selector; see Catalog selectors. | | `identity` | yes | `NappletIdentity` | Verified napplet identity. | | `title` | no | text | Human-readable title. | | `description` | yes | text | Non-empty plain-text manifest content. | | `requires` | yes | list of text | Bare domains from manifest R tags. | | `optional` | yes | list of text | Bare domains from manifest O tags. | | `archetypes` | yes | list of `ArchetypeDescriptor` | Advertised roles. Empty for a napplet with no archetype. | `ArchetypeHandler` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `archetype` | yes | text | NAAT role slug. | | `currentHandler` | yes | text or null | `id` of a returned napplet selected for implicit dispatch, or null when no such target is available to report. | `CatalogSnapshot` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `napplets` | yes | list of `NappletDescriptor` | Napplets available to the caller. | | `handlers` | yes | list of `ArchetypeHandler` | Current handler state for every archetype advertised by the returned napplets. | #### Catalog selectors Every returned napplet MUST have a unique `id` within the runtime's catalog. The runtime MUST NOT reuse an identifier for a different napplet during the runtime session. The identifier is opaque: consumers MUST preserve it exactly and MUST NOT derive a selector from `identity.dTag` or any other identity field. The structured `identity` describes the verified version; it is not a selector. When the runtime implements `intent`, selectors MUST use the [NAP-INTENT catalog identifier contract](https://github.com/napplet/naps/blob/1020ae53884a177b7ffc58a679126bc8800d0c6d/naps/NAP-INTENT.md#catalog-identifiers). For each napplet returned by both domains, the catalog `id` MUST equal its `IntentCandidate.id`. This does not require role-less entries to appear in intent discovery. A caller MAY pass it unchanged as an explicit `handler` to `intent.invoke` when that capability is exposed and the user authorizes the selection. Receiving a catalog entry or `currentHandler` MUST NOT grant that authorization. Catalog snapshots do not reserve a target, pin its artifact version, or exempt later invocations from convention compatibility and policy checks. A non-null `currentHandler` MUST equal exactly one returned entry's `id`. If the selected entry is omitted, the runtime MUST report null, not substitute another napplet. Without an intent resolver, handler values MUST be null; napplet metadata remains queryable. A caller that needs to preserve the reported target MUST use the explicit selector, not repeat implicit dispatch or fall back to a `d` tag if selection fails. #### Manifest mapping The catalog MUST use verified [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) manifests of kinds 5129, 15129, and 35129. It MUST derive roles from `z` tags and accepted conventions and parameter names from `i` tags. Each intent is associated with the role matching its archetype segment on the same manifest, independent of tag order. Invalid or unmatched intents contribute no dispatch contract; roles with no matching intent have an empty `intents` list. Convention identities remain queryless and fragment-free. Legacy combined archetype tags do not substitute for these advertisements. Trailing `i` elements are names only. The runtime MUST NOT infer parameter types, requiredness, descriptions, or event-kind constraints from them. Payload semantics belong to the named convention. An empty parameter list does not prohibit an explicit payload. Titles come from `title`; descriptions come from plain-text manifest `content`. Required and optional domain lists come from `R` and `O`. Advertisements and capability declarations MUST NOT widen runtime grants. The runtime MUST inspect the complete required set when assessing compatibility; missing optional domains alone MUST NOT exclude a napplet. Identity MUST distinguish publishers and manifest kinds. Named identities are keyed by publisher, kind, and `d`; root identities by publisher and kind; snapshot identities by signed event id. `eventId` and `artifactHash` describe the resolved version. Catalogs MUST NOT require `dTag` on root or snapshot entries. **`get()`** — Returns one point-in-time snapshot. The runtime applies caller policy before constructing the result. An empty `napplets` list is a successful result when no napplets are available to the caller. ### Wire Protocol `catalog.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `catalog.get` | napplet -> shell | `id` | | `catalog.get.result` | shell -> napplet | `id`, `snapshot`?, `error`? | Key design notes: - The request and result use `id` for correlation. - The snapshot is complete for the caller at the time the runtime handles the request. - Convention identities MUST be queryless. Parameter descriptors are separate metadata and MUST NOT be appended to a convention identity. - A `currentHandler` identifies the implicit dispatch target. It is not a window, process, or running-instance identifier. #### Examples **Get the current catalog:** ``` -> { "type": "catalog.get", "id": "c1" } <- { "type": "catalog.get.result", "id": "c1", "snapshot": { "napplets": [ { "id": "catalog-noteview", "identity": { "kind": 35129, "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "eventId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "dTag": "noteview", "artifactHash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" }, "title": "Note View", "description": "Displays a Nostr note.", "requires": ["relay"], "optional": ["theme"], "archetypes": [ { "archetype": "note", "intents": [ { "intent": "open", "convention": "napplet:note/open", "parameters": ["id"] } ] } ] } ], "handlers": [ { "archetype": "note", "currentHandler": "catalog-noteview" } ] } } ``` #### Error Handling `catalog.get.result` MAY include `error` when the runtime cannot construct the catalog. When `error` is present, `snapshot` MUST be omitted. ``` <- { "type": "catalog.get.result", "id": "c1", "error": "catalog unavailable" } ``` ### Shell Behavior - The shell MUST source the catalog from napplet manifests it has resolved and verified. It MUST NOT trust identity fields supplied by a running napplet. - The shell MUST report availability from its catalog, not from currently running instances. - The shell MUST include napplets with no archetype when they are available to the caller. - The shell MUST preserve queryless convention identities exactly. It MUST NOT normalize, prefix-match, or wildcard-match them. Parsing an intent to validate its archetype segment MUST NOT change its stored identity. - The shell MUST keep parameter metadata separate from convention identity. It MUST NOT report structured payload fields as query parameters. - The shell MUST include one `ArchetypeHandler` for every archetype advertised by the returned napplets. `currentHandler` MUST reference a returned napplet's `id` or be null, as specified in Catalog selectors. - The shell MUST respond to every request with a result carrying the same `id`. - The shell MUST return every required field of each included record. When a required field cannot be verified or disclosed under caller policy, it MUST omit the entire napplet entry. It MUST NOT return a partial descriptor or fabricate a required value. This includes `dTag` for kind 35129; `dTag` MUST be absent for kinds 5129 and 15129. - The shell MAY omit optional fields it cannot verify or disclose. Absence of optional manifest declarations produces the specified empty lists, not omitted fields or an invalid entry. - The shell MAY scope or redact the catalog according to caller policy, subject to the required-field and handler-reference rules above. It MUST construct `handlers` from the entries that remain after filtering. - The shell MAY enforce ACL checks on catalog access. ### Security Considerations - A napplet catalog is a fingerprinting surface. The shell SHOULD expose only entries and metadata the caller needs. - Titles, descriptions, intent names, and parameter names are untrusted display text. Consumers MUST escape them before rendering. - Handler preferences are user state. NAP-CATALOG is read-only and MUST NOT let a napplet set or change them. - Catalog identity comes from verified manifests. A running napplet cannot alter its verified manifest fields or `artifactHash` through this interface. - Parameter metadata does not validate a delivered payload. A receiving napplet MUST validate payload data according to the named convention. ### Implementations - (none yet) ### Changelog - `5711b2e` - Defined read-only napplet discovery, convention parameter metadata, and current archetype handler state. - `1c076ce` - Aligned catalog identities, z/i advertisements, parameter-name metadata, content descriptions, and R/O capabilities with current manifests. - `b696bef` - Added opaque selectors shared with intent resolution, changed currentHandler to reference entry IDs, declared optional intent layering, and required complete descriptors with consistent filtering. [Join the discussion · PR #95 ↗](https://github.com/napplet/naps/pull/95)[Read proposed changes ↗](https://github.com/napplet/naps/pull/95/files) On this pageSpecification metadata Capability domain `catalog` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies - [intent](https://napplet.run/protocol/nap-intent/) Source revision `1020ae53884a` [View source on GitHub ↗](https://github.com/napplet/naps/pull/95/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CLASS-1 / PR #17 # Strict Baseline Class Draft PRDeferred upstream The registry marks NAP-CLASS as deferred. This proposal is dormant and should not be treated as an available protocol capability. This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/b9128e0cb61138680c0a76061174fb6d014c0d93/naps/NAP-CLASS-1.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/17/files). ## NAP-CLASS-1 ### Strict Baseline Class `draft` **NAP ID:** NAP-CLASS-1 **Parent:** NAP-CLASS **Class number:** 1 ### Description NAP-CLASS-1 is the strict baseline posture for napplets that do not declare any class-contributing NAP capabilities. Shells emit a restrictive Content Security Policy with `connect-src 'none'`, show no consent prompt, and send `class.assigned` with `class: 1`. This is the default class for any napplet whose manifest lacks tags from NAPs that would promote it to a higher class. ### CSP Posture ``` connect-src 'none' ``` Shells emitting the NAP-CLASS-1 posture MUST include `connect-src 'none'` in the runtime CSP served with the napplet's HTML. The directive is the signal that the napplet has no user-approved direct-network origins. Other directives in the baseline CSP (e.g., `script-src`, `default-src`, `img-src`) are shell-policy concerns and are NOT specified by NAP-CLASS-1 — only the `connect-src` value is the class's defining characteristic. ### Manifest Prerequisites NAP-CLASS-1 is the default posture. A napplet manifest reaches NAP-CLASS-1 when no other class-contributing NAP's trigger conditions are met. Future `NAP-CLASS-$N` entries (with `$N > 1`) MUST document their own trigger conditions; absence of all such triggers means the napplet is NAP-CLASS-1. There is no consent state associated with NAP-CLASS-1 and therefore no user-consent prerequisite. A napplet resolves to this class at first load, at every subsequent load, and under every shell that implements the base protocol — independent of user action. ### Shell Responsibilities - Shell MUST emit a runtime CSP header containing `connect-src 'none'` for the napplet's HTML response. - Shell MUST NOT prompt the user for any network-access consent when serving a NAP-CLASS-1 napplet — there is nothing to consent to, and raising a prompt would mislead the user into believing a choice is available when none is. - Shell MUST send exactly one `class.assigned` envelope with payload `{ class: 1 }` at iframe-ready time per NAP-CLASS's wire-timing rules. A shell that implements `nap:class` MUST deliver the envelope even when the value is `1`; silence is not the same signal as explicit assignment. - Shell SHOULD treat residual `` in NAP-CLASS-1 napplet HTML as harmless (CSP intersection of two `connect-src 'none'` values is still `'none'`), but MAY log a deployment warning to encourage napplet authors to migrate away from meta-CSP emission. Shells MUST NOT refuse-to-serve a NAP-CLASS-1 napplet solely on the basis of residual meta-CSP. - Shell MUST NOT add sandbox relaxations or CSP directives that would cause a NAP-CLASS-1 napplet to behave distinguishably from another NAP-CLASS-1 napplet. The posture is uniform across all napplets in this class. ### Security Considerations #### Strictness floor, not a ceiling NAP-CLASS-1 is the minimum-trust posture. It is appropriate for napplets with no network requirements, napplets that rely exclusively on shell-mediated resource fetching (via other NAPs that provide proxied resource access), or napplets in development/test environments where network egress is disabled by policy. A shell that wishes to apply a weaker posture than NAP-CLASS-1 cannot do so while claiming conformance to this class — the `connect-src 'none'` directive is not a default, it is a guarantee. A weaker posture is a different class and MUST be assigned a different integer. #### CSP intersection Because the browser enforces CSP as an intersection of header and meta directives, a NAP-CLASS-1 napplet that accidentally ships meta-CSP with `connect-src 'none'` will still behave correctly — the intersection of `'none'` and `'none'` is `'none'`. This is why shells treat residual meta-CSP as harmless in the NAP-CLASS-1 case but refuse-to-serve in higher classes: a meta-CSP with `connect-src 'none'` silently suppresses any header-level grant, which is catastrophic for classes that rely on granted origins but a no-op here. #### No grant state NAP-CLASS-1 has no persistent state — no consent decision, no origin list, no `(dTag, aggregateHash)` key. Every napplet resolving to NAP-CLASS-1 is indistinguishable from every other at the class level. This makes the class a safe default: a shell may assign it silently, without interrupting the user, without persisting any record that would outlive the session. #### Compatibility with other NAPs NAP-CLASS-1 is deliberately compatible with every other NAP a shell may choose to implement. A shell may grant a NAP-CLASS-1 napplet access to, for example, a shell-mediated storage NAP or a shell-mediated relay NAP; the class does not forbid this. What the class forbids is the napplet reaching external network origins directly from the frame — everything network-sourced MUST flow through a shell-mediated NAP's `postMessage` surface. ### References - `NAP-CLASS.md` — parent spec; defines the `class.assigned` envelope, the `window.napplet.class` runtime surface, and the authoring rules for track members including NAP-CLASS-1. - NIP-5D — Napplet wire format. ### Changelog - `71ad6a1` - Introduced NAP-CLASS-1 as the strict baseline posture sub-track. [Join the discussion · PR #17 ↗](https://github.com/napplet/naps/pull/17)[Read proposed changes ↗](https://github.com/napplet/naps/pull/17/files) On this pageSpecification metadata Capability domain `Not declared` Repository state Draft PR Registry status Deferred Document marker draft Declared dependencies None declared in the preamble Source revision `b9128e0cb611` [View source on GitHub ↗](https://github.com/napplet/naps/pull/17/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CLASS-2 / PR #18 # User-Approved Explicit-Origin Class Draft PRDeferred upstream The registry marks NAP-CLASS as deferred. This proposal is dormant and should not be treated as an available protocol capability. This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/45c9b12dcb87ba574b1c6fa8b73b22f5b307b5da/naps/NAP-CLASS-2.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/18/files). ## NAP-CLASS-2 ### User-Approved Explicit-Origin Class `draft` **NAP ID:** NAP-CLASS-2 **Parent:** NAP-CLASS **Class number:** 2 ### Description NAP-CLASS-2 is the posture for napplets that declare explicit direct-network origins and have received user consent for those origins. Shells emit a runtime CSP with `connect-src `, prompt the user on first load, persist the decision keyed on `(dTag, aggregateHash)`, and send `class.assigned` with `class: 2` when approved. If the user denies the prompt, the napplet is served under the NAP-CLASS-1 posture instead (`class: 1`, `connect-src 'none'`) — denied napplets are indistinguishable from napplets that never declared origins. ### CSP Posture ``` connect-src ``` Shells emitting the NAP-CLASS-2 posture MUST include `connect-src ` in the runtime CSP, where `` is the user-approved origin list for the napplet identified by `(dTag, aggregateHash)`. The origin list MUST be exactly the set the user approved — shells MUST NOT add, remove, or normalize origins between the approved set and the emitted directive. Other CSP directives in the baseline are shell-policy concerns; only the `connect-src` value is the class's defining characteristic. If the approved set is empty (the user approved zero origins on the prompt), the shell MUST emit `connect-src 'none'` and SHOULD downgrade the assignment to NAP-CLASS-1 — an empty-set NAP-CLASS-2 is behaviorally indistinguishable from NAP-CLASS-1 and introduces no value. Shells MAY retain the NAP-CLASS-2 assignment for an empty approved set if doing so materially affects downstream behavior (for example, to record that the user did review a prompt), but the CSP surface is identical to NAP-CLASS-1 in that case. ### Manifest Prerequisites A napplet manifest reaches NAP-CLASS-2 when it declares at least one tag from a class-contributing NAP whose trigger condition for NAP-CLASS-2 is met, AND the user has approved the napplet's request at first load. The specific manifest tag shape and origin format are defined by the individual class-contributing NAP (see that NAP's spec for the manifest-tag surface). NAP-CLASS-2 itself does not define any manifest tags — it defines the posture. Validation of the origin set produced by the class-contributing NAP (syntactic shape, canonicalization, scheme whitelist, and similar rules) is likewise that NAP's responsibility. NAP-CLASS-2 consumes only the user-approved output of that process, emits it as `connect-src`, and enforces the grant's persistence rules; it does not re-validate the origins. ### Shell Responsibilities - Shell MUST prompt the user at first load of any napplet matching `(dTag, aggregateHash)` that is not already in an approved-or-denied state. The prompt MUST identify the napplet and MUST enumerate the concrete origins under consideration literally, without summarization or first-presentation elision. - Grant persistence MUST be keyed on the exact `(dTag, aggregateHash)` composite; shells MUST NOT key on `dTag` alone. Keying on `dTag` alone would allow a rebuilt napplet (new `aggregateHash`) to inherit a prior approval silently — this is a security bug, not a UX optimization. - Shell MUST emit a runtime CSP header containing `connect-src ` when serving an approved NAP-CLASS-2 napplet. - Shell MUST send exactly one `class.assigned` envelope with payload `{ class: 2 }` at iframe-ready time for approved napplets. For denied napplets, the shell MUST send `class.assigned` with `{ class: 1 }` — denied napplets are served under the NAP-CLASS-1 posture, and the wire must reflect that. - Shell MUST refuse-to-serve any NAP-CLASS-2 napplet whose HTML contains ``. Browser CSP intersection silently reduces header `connect-src ` AND meta `connect-src 'none'` to `'none'`, suppressing the grant without any diagnostic to the user or the napplet author. The refuse-to-serve diagnostic MUST identify the offending meta element and point the napplet author at rebuild guidance. - Shell MUST expose a user-facing revocation affordance for approved grants. Revoked grants MUST move to a DENIED state (not deleted) so shells retain historical knowledge and can offer a re-approve path without re-soliciting a fresh prompt from scratch. - Shell SHOULD reload the napplet's iframe on revocation — the next load will emit `class.assigned` with `class: 1` and restrictive CSP. In-session mid-lifecycle class re-assignment is out of scope (enforced by NAP-CLASS's at-most-one-envelope rule); revocation during an active lifecycle is allowed to take effect only at the next frame creation. - Shell MUST NOT rewrite, reorder, or merge origin tokens between the consent prompt the user saw and the CSP it emits. The approved set is byte-for-byte the set the user read. ### Grant Persistence Semantics Grants are keyed on the composite `(dTag, aggregateHash)`. Rebuilding the napplet with any change that alters `aggregateHash` (including changes to the declared origin set from any class-contributing NAP) produces a new key and auto-invalidates the prior grant — the user is prompted fresh on the next load. This is the primary defense against silent supply-chain upgrade: a prior approval for `(dTag, v1)` does not automatically extend to `(dTag, v2)`. Shells SHOULD show a diff UI on re-prompt when the same `dTag` has been previously approved but the `aggregateHash` has changed: "previously approved origins X; now requesting origins Y." This is a UX floor for informed re-consent — users who approved v1 should be able to understand what changed in v2 before approving again. Shells MUST NOT present the re-prompt as if it were a first prompt when a prior grant exists under the same `dTag`; the historical context matters. Shells MAY extend the keying tuple (for example, to include additional attributes such as author pubkey or network partition), but an extended key MUST be a strict superset of `(dTag, aggregateHash)` — the two components MUST remain present and MUST continue to carry the semantics described above — so that grants remain portable across shells that implement this class conformantly. A shell that keys grants on a subset of `(dTag, aggregateHash)` is non-conformant to NAP-CLASS-2. ### Security Considerations #### Post-grant opacity Once origins are approved, the shell has no browser-level hook to inspect or filter subsequent network traffic between the napplet and the approved origins. This is a fundamental tradeoff of direct-network grants. Consent-prompt language MUST reflect this: the user is approving the napplet to send and receive any data with the listed servers; the shell cannot see or filter that traffic. Consent UI that understates the scope of the grant is misleading the user. #### Cleartext origins Shells MAY refuse cleartext schemes (`http:`, `ws:`) entirely, and SHOULD document their policy. When cleartext is permitted, the consent prompt MUST visibly mark cleartext origins so the user can evaluate the confidentiality tradeoff. Cleartext origins granted from an HTTPS shell are subject to mixed-content rules imposed by the browser and may silently fail at connect time; that failure is orthogonal to the consent layer but may surprise the napplet author. #### Composite-key hygiene A shell that keys grants on `dTag` alone would allow a rebuilt napplet (new `aggregateHash`) to inherit a prior approval silently — this is a security bug, specifically a silent-supply-chain-upgrade vector. The `(dTag, aggregateHash)` composite is load-bearing. A shell that elides the hash, or that treats the hash as advisory, is not implementing NAP-CLASS-2 — it is implementing a weaker posture that MUST be assigned its own class number. #### Residual meta-CSP is a Class-2 project-killer Unlike NAP-CLASS-1, where residual meta-CSP is harmless (intersection of `'none'` and `'none'` is `'none'`), a NAP-CLASS-2 napplet with residual meta-CSP has its granted origins silently suppressed by browser CSP intersection (intersection of `` and `'none'` is `'none'`). The user sees a prompt, approves origins, and then the napplet still cannot reach those origins — with no error message pointing at the cause. The refuse-to-serve MUST exists specifically to prevent this silent-failure mode. A shell that does not implement the refuse-to-serve is not implementing NAP-CLASS-2 faithfully, even if every other surface is correct. #### Revocation timing Revocation of an active grant MUST take effect no later than the next time the affected `(dTag, aggregateHash)` is loaded. Shells are not required to tear down an already-running napplet iframe on revocation (NAP-CLASS's at-most-one-envelope rule forbids in-session class re-assignment), but they MUST NOT emit the revoked origins in `connect-src` for any napplet frame created after the revocation is acknowledged. A shell that continues to serve revoked origins to new frames is non-conformant. #### Denied napplets are Class-1 napplets A user who denies the NAP-CLASS-2 consent prompt produces a napplet that is served identically to a NAP-CLASS-1 napplet: `connect-src 'none'`, `class.assigned` with `class: 1`. This is deliberate. The napplet MUST NOT be able to detect, from its own runtime surface, whether it is running as a "genuine" NAP-CLASS-1 or as a denied NAP-CLASS-2. Allowing the napplet to distinguish would let it pressure the user — for example, by refusing to function meaningfully under the denied-equivalent posture — and would convert the consent prompt from a binary decision into a coercive one. ### References - `NAP-CLASS.md` — parent spec; defines the `class.assigned` envelope, the `window.napplet.class` runtime surface, and the authoring rules for track members including NAP-CLASS-2. - `NAP-CLASS-1.md` — sibling track member; the posture a denied NAP-CLASS-2 napplet is served under. - NIP-5D — Napplet wire format. ### Changelog - `19feb49` - Introduced NAP-CLASS-2 as the user-approved explicit-origin posture sub-track. [Join the discussion · PR #18 ↗](https://github.com/napplet/naps/pull/18)[Read proposed changes ↗](https://github.com/napplet/naps/pull/18/files) On this pageSpecification metadata Capability domain `Not declared` Repository state Draft PR Registry status Deferred Document marker draft Declared dependencies None declared in the preamble Source revision `45c9b12dcb87` [View source on GitHub ↗](https://github.com/napplet/naps/pull/18/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CLASS / PR #16 # Napplet Class Authority Draft PRDeferred upstream The registry marks NAP-CLASS as deferred. This proposal is dormant and should not be treated as an available protocol capability. This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/4828279800dc1696452abd329a24ddb37e292bc5/naps/NAP-CLASS.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/16/files). ## NAP-CLASS ### Napplet Class Authority `draft` **NAP ID:** NAP-CLASS **Domain:** `class` **Web binding (NIP-5D):** `window.napplet.class` · `shell.supports("class")` **Parent:** NIP-5D ### Description NAP-CLASS establishes an abstract "napplet class" concept — an integer number assigned by the shell at iframe-ready time, delivered over the wire, and readable by the napplet at `window.napplet.class`. The class captures the shell's determination of the napplet's security posture: CSP shape, consent state, any additional shell-enforced constraints a concrete posture cares to name. This NAP OWNS the class-track machinery; concrete class definitions live in sibling `NAP-CLASS-$N` documents. The shell is the sole authority on class. Class is determined by which class-contributing NAPs are declared in the napplet manifest combined with any user-consent outcomes. The shell MUST send exactly one `class.assigned` envelope per napplet lifecycle, after the iframe signals readiness and before the napplet has an opportunity to meaningfully branch on class. Dynamic mid-session re-classification is out of scope for v1; shells that need to change class for an already-running napplet MUST tear down the frame and create a new one. This NAP is voluntary. Napplets MUST gracefully degrade when `window.napplet.class` is `undefined` — which happens either because the shell does not implement `nap:class`, or because the `class.assigned` envelope has not yet arrived. Cross-NAP invariants (for example, in a shell implementing both NAP-CLASS and some class-contributing NAP, that the advertised class matches the contributing NAP's runtime state) are shell responsibilities documented by the NAPs that participate. NAP-CLASS itself has no opinion on how a given integer is derived; it only specifies how that integer is communicated. ### API Surface `NappletClassState`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `class` | `uint` | no | Shell-assigned class number; absent until assigned or unsupported. | Napplets read `window.napplet.class`. Before depending on the value, napplets SHOULD check `shell.supports('nap:class')`; a shell that does not advertise this capability will never populate the field. Once the field has been populated by the shell's `class.assigned` envelope, it MUST NOT change for the remainder of the napplet's lifetime. ### Wire Protocol `class.*` messages use the NIP-5D wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `class.assigned` | shell -> napplet | `id`, `class` | Normative constraints on `class.assigned`: - The shell MUST send `class.assigned` after the iframe signals readiness (shim bootstrap complete) and before any other napplet-bound envelope for that iframe. The napplet's first observation of a shell-originated envelope for the current lifecycle SHOULD be `class.assigned`. - The shell MUST send AT MOST ONE `class.assigned` envelope per napplet lifecycle. Dynamic re-classification is out of scope for v1. A second `class.assigned` for the same napplet is a protocol violation; conformant napplet shims MAY silently drop it, and SHOULD surface a diagnostic. - The `id` field is a freshly minted correlation identifier per NIP-5D envelope conventions. There is no napplet→shell response envelope for `class.assigned`; the correlation id exists for consistency with other NIP-5D envelopes, not because a response is expected. - The `class` field is a non-negative integer. The integer value selects a concrete class definition from the `NAP-CLASS-$N` sub-track (e.g., `class: 1` selects `NAP-CLASS-1`). No further payload fields are defined; shells MUST NOT attach class-specific policy configuration to this envelope. - The shell MUST NOT use `class.assigned` as a configuration channel. Policy semantics belong to the corresponding track member; the integer is the only wire-level signal. #### Example ``` shell -> napplet: { "type": "class.assigned", "id": "c1", "class": 2 } ``` The napplet shim writes `2` into `window.napplet.class` upon receipt and consults `NAP-CLASS-2.md` for the semantics of that class. ### NAP-CLASS-$N Sub-Track The NAP-CLASS track is the set of documents named `NAP-CLASS-$N.md`, where `$N` is the non-negative integer carried by `class.assigned`. Each track member is a standalone normative spec that fully defines one security posture. A shell implementer implementing class `N` correctly MUST be able to do so by reading only `NAP-CLASS.md` and `NAP-CLASS-$N.md`. #### Naming rules - Class numbers are non-negative integers. - Integers ascend (1, 2, 3, ...). The next published class is one greater than the largest previously published class. - A class number MUST NOT be reused once published to the public NAPs repo. A published class may be superseded by a later class, but its integer remains retired. - A class number MUST NOT be skipped without spec-level justification. If a gap is intentional, the gap number MUST exist as a `NAP-CLASS-$N.md` document marked `reserved` in its status banner, explaining why the number is held. - File names are always `NAP-CLASS-$N.md` (e.g., `NAP-CLASS-1.md`, `NAP-CLASS-2.md`). Everything participating in the track is under the `NAP-` namespace. #### Required content structure for each sub-track member Every `NAP-CLASS-$N.md` MUST include, in order: 1. **Setext heading + subtitle + status banner** matching the shape of NAP-CLASS.md (setext `=` underline for the title, setext `-` underline for the subtitle, backticked status banner, metadata block). 2. **Description** — at minimum a single paragraph naming the posture in one sentence and identifying its role in the track. 3. **CSP posture** — the exact CSP directive shape the shell MUST emit for napplets assigned to this class, stated as wire shape rather than as prose. At minimum, the `connect-src` shape; other directives are optional for the class to name. 4. **Manifest prerequisites** — the conditions a napplet manifest MUST satisfy to be assigned to this class (what class-contributing NAPs or tags trigger the class, and any user-consent prerequisites). 5. **Shell responsibilities** — enumerated MUSTs the shell MUST honor at iframe serve time (consent prompt? emit grant-derived header? persist state? refuse-to-serve conditions?). 6. **Security considerations** — the threat model the posture defeats and the attacks it explicitly does not defeat. 7. **References** — cites `NAP-CLASS.md` as parent, and any additional context the track member relies on. A track member SHOULD additionally include worked example CSP strings, compatibility notes (how napplets authored against other classes interoperate), and a "Assignment" subsection reiterating that class communication is exclusively via `class.assigned`. #### Citation conventions NAPs and other specs MUST cite class documents by file name (for example, "see `NAP-CLASS-2.md`"). They MUST NOT use the abstract phrase "Class 2" or "class 2" as the primary reference. File-name citations prevent prose drift when a class document is renamed, retired, or superseded; abstract phrasings outlive the document they nominally refer to. A spec that needs to discuss the conceptual integer without pointing at a specific member (for example, when discussing the track as a whole) SHOULD use the phrase "the class number" or "the integer carried by `class.assigned`" rather than "Class N". Specific members are always cited by file name. #### Adding a new class Authors proposing a new class MUST open a PR to the public `napplet/naps` repo containing (a) a new `NAP-CLASS-$N.md` file following the required content structure above, and (b) an update to this section documenting the new entry in the track. The class number is assigned at merge time by the NAPs-track maintainers, following the ascending-integer rule. A proposed class whose integer is already in use MUST be renumbered before merge. ### Capability Advertisement Shells implementing this NAP MUST advertise `shell.supports('nap:class') === true`. Napplets MUST NOT depend on `window.napplet.class` having a value; they SHOULD check `shell.supports('nap:class')` first. If the shell does not advertise the capability, the napplet MUST fall back to treating itself as unclassified — assuming the most restrictive defaults the napplet can function under. A shell that advertises `nap:class` but fails to deliver `class.assigned` is non-conformant; a shell that simply does not advertise `nap:class` is conformantly unclassified. ### Security Considerations #### Shell is sole authority Napplet code MUST NOT attempt to infer its own class from the environment — from the CSP it observes, from the capabilities the shell advertises, from the presence or absence of other NAPs. The shell's `class.assigned` envelope is the only authoritative signal. A napplet that ignores `class.assigned` and assumes a class risks being confused between a strict-posture shell and a permissive-posture shell, and may make unsafe assumptions about capabilities the shell has not granted. The integer carried on the wire is a reflection of the shell's enforcement decision, not a substitute for it. Sandboxing, CSP, and consent MUST be applied at frame creation time, before any napplet code runs. A malicious or buggy napplet that ignores `window.napplet.class` still runs under whatever posture the shell enforced; the integer is informational to the napplet, not instructive to the shell. #### At-most-one envelope The at-most-one-terminal-envelope-per-lifecycle constraint prevents races where the napplet's behavior depends on late-arriving class signals, and prevents a compromised napplet context from prompting the shell for a re-assignment mid-session. Shells MUST NOT emit a second `class.assigned` to "upgrade" or "downgrade" a class mid-session. If a shell wishes to change posture, it MUST tear down the current napplet iframe and create a new one, at which point the new iframe's lifecycle begins with its own `class.assigned` envelope. #### Cross-NAP invariants are shell responsibilities NAPs that contribute to class determination document their own invariants (for example, "a napplet with this NAP's tags and granted consent takes on class `N`"). Enforcing those invariants is a shell obligation, not a NAP-CLASS obligation. If a shell implements a class-contributing NAP but advertises a class integer inconsistent with that NAP's runtime state, the shell is non-conformant to the class-contributing NAP — NAP-CLASS itself has no opinion on whether any particular integer is "correct" for a given napplet. NAP-CLASS only specifies how the integer is communicated. #### Graceful degradation `window.napplet.class === undefined` is a valid and well-defined state. It means one of: the shell does not implement `nap:class`; the shell implements it but has not yet delivered `class.assigned`; the napplet is running in an environment that does not implement the protocol at all. Napplets that branch on class MUST handle the undefined case as "unclassified — assume the most restrictive defaults the napplet can function under." Napplets MUST NOT assume that a missing class means any particular posture is active; the correct assumption is always the most restrictive one the napplet can support. ### References - NIP-5D — Napplet wire format (JSON envelope over `postMessage`); parent spec for this NAP. - `NAP-CLASS-1.md` — strict baseline posture; track member for `class: 1`. - `NAP-CLASS-2.md` — user-approved explicit-origin posture; track member for `class: 2`. ### Changelog - `78f9157` - Introduced NAP-CLASS as the napplet class authority sub-track root. [Join the discussion · PR #16 ↗](https://github.com/napplet/naps/pull/16)[Read proposed changes ↗](https://github.com/napplet/naps/pull/16/files) On this pageSpecification metadata Capability domain `class` Repository state Draft PR Registry status Deferred Document marker draft Declared dependencies None declared in the preamble Source revision `4828279800dc` [View source on GitHub ↗](https://github.com/napplet/naps/pull/16/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-COMMON / PR #67 # Common Social Actions Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** Social action binding is tested; broader host integration and evolving action semantics warrant explicit opt-in. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/67), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/8cd0eae8550575771bb616a384e2dfbcade31673/naps/NAP-COMMON.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/67/files). ## NAP-COMMON ### Common Social Actions `draft` **NAP ID:** NAP-COMMON **Domain:** `common` **Depends:** - `identity` — capability · required — user-authoritative actions use the current shell-user identity as the event author. - `relay` — capability · required — fetches kind 0 profiles and publishes kind 3 follow lists, kind 7 reactions, and kind 1984 reports. - `relay` — wire · required — imports `RelayEventResult` for raw profile event returns. - `resource` — wire · optional — imported `RelayEventResult.sidecar.resources?` carries `ResourceSidecarEntry[]`, a type owned by the `resource` domain. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.common`; domain presence signals availability. ### Description NAP-COMMON is a small shell-mediated surface for public Nostr identifier helpers, profile lookup, and common social actions. It does not expose generic event construction, relay selection, moderation policy, or private-key material. The napplet supplies intent and minimal targets. The shell owns user identity, consent, signing, replacement-list merging, publishing, lookup policy, and NIP normalization. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `encodeNip19` | `input` (`CommonNip19EncodeInput`) | `CommonNip19EncodeResult` | `common.encodeNip19` / `.result` | | `decodeNip19` | `value` (`tstr`) | `CommonNip19DecodeResult` | `common.decodeNip19` / `.result` | | `getProfile` | `target` (`CommonProfileTarget`) | `CommonProfileResult` | `common.getProfile` / `.result` | | `follows` | none | `[ * HexPubkey ]` | `common.follows` / `.result` | | `follow` | one or more `npub` targets | `CommonActionResult` | `common.follow` / `.result` | | `unfollow` | one or more `npub` targets | `CommonActionResult` | `common.unfollow` / `.result` | | `react` | `id`, `reaction`, `customEmojiHref?` | `CommonActionResult` | `common.react` / `.result` | | `report` | `target`, `reason`, `text` | `CommonActionResult` | `common.report` / `.result` | #### Schemas `RelayEventResult` is owned by NAP-RELAY. NAP-COMMON imports it by name for raw kind 0 profile events returned by `getProfile`. Shell-created action events remain plain `NostrEvent` values because they are not relay-read results. Primitive aliases: | Name | Definition | | --- | --- | | `Npub` | NIP-19 `npub` string | | `Nprofile` | NIP-19 `nprofile` string | | `HexPubkey` | 32-byte public key encoded as lowercase hex | | `NostrEventId` | 32-byte event id encoded as lowercase hex | | `NostrEvent` | Nostr event object | | `CommonPubkeyList` | non-empty list of `Npub` values | | `CommonProfileTarget` | `HexPubkey`, `Npub`, or `Nprofile` | | `RelayEventResult` | external type owned by NAP-RELAY | Enums: | Name | Values | | --- | --- | | `CommonNip19Type` | `npub`, `note`, `nprofile`, `nevent`, `naddr`, `nrelay` | | `CommonReaction` | `+`, `-`, or text | | `CommonReportReason` | `nudity`, `malware`, `profanity`, `illegal`, `spam`, `impersonation`, `other` | `CommonNip19EncodeInput` variants: | Variant | Required fields | Optional fields | | --- | --- | --- | | `npub` | `type: "npub"`, `hex: tstr` | none | | `note` | `type: "note"`, `hex: tstr` | none | | `nprofile` | `type: "nprofile"`, `pubkey: HexPubkey` | `relays: list of tstr` | | `nevent` | `type: "nevent"`, `eventId: NostrEventId` | `relays: list of tstr`, `author: HexPubkey`, `kind: uint` | | `naddr` | `type: "naddr"`, `identifier: tstr`, `pubkey: HexPubkey`, `kind: uint` | `relays: list of tstr` | | `nrelay` | `type: "nrelay"`, `relay: tstr` | none | `CommonNip19EncodeResult` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | | `value` | no | text | | `nip19Type` | no | `CommonNip19Type` | | `error` | no | text | `CommonNip19DecodeResult` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | | `nip19Type` | no | `CommonNip19Type` | | `hex` | no | text | | `pubkey` | no | `HexPubkey` | | `eventId` | no | `NostrEventId` | | `identifier` | no | text | | `relays` | no | list of text | | `author` | no | `HexPubkey` | | `kind` | no | unsigned integer | | `relay` | no | text | | `error` | no | text | `CommonProfileData` fields: | Field | Required | Type | | --- | --- | --- | | `name` | no | text | | `displayName` | no | text | | `about` | no | text | | `picture` | no | text | | `banner` | no | text | | `nip05` | no | text | | `lud16` | no | text | | `website` | no | text | | additional fields | no | any | `CommonProfileResult` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | | `pubkey` | yes | `HexPubkey` | | `profile` | no | `CommonProfileData` or null | | `result` | no | `RelayEventResult` | | `error` | no | text | `CommonActionResult` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | | `eventId` | no | text | | `event` | no | `NostrEvent` | | `error` | no | text | `CommonReportTarget` variants: | Variant | Required fields | Optional fields | | --- | --- | --- | | event | `type: "event"`, `id: tstr` | `pubkey: tstr`, `relay: tstr` | | pubkey | `type: "pubkey"`, `pubkey: Npub or HexPubkey` | `relay: tstr` | #### Operation Rules | Operation | Rules | | --- | --- | | `encodeNip19` | Encodes public NIP-19 identifiers: `npub`, `note`, `nprofile`, `nevent`, `naddr`, `nrelay`. MUST reject `nsec`. | | `decodeNip19` | Decodes the same public types. MUST reject `nsec`, ignore unknown TLVs, and SHOULD reject strings longer than 5000 chars. | | `getProfile` | Accepts hex pubkey, `npub`, or `nprofile`. Normalizes to hex, treats `nprofile` relay TLVs as hints, queries latest kind 0, returns the raw profile event as `RelayEventResult`, and returns `profile: null` if not found. | | `follows` | Returns hex pubkeys from the shell-user's current NIP-02 kind 3 follow list. Returns an empty array when no list is found. | | `follow` | Decodes each `npub`, merges new `p` tags into the current NIP-02 kind 3 list, preserves unrelated tags, signs, publishes. Idempotent. | | `unfollow` | Decodes each `npub`, removes matching `p` tags from the current kind 3 list, preserves unrelated tags, signs, publishes. Idempotent. | | `react` | Publishes NIP-25 kind 7 for native Nostr events only. `reaction` is `+`, `-`, one Unicode emoji, or one NIP-30 shortcode. `customEmojiHref` requires one matching `emoji` tag. | | `report` | Publishes NIP-56 kind 1984. Pubkey reports use `p` with reason. Event reports use `e` with reason plus author `p`; shell MUST resolve or reject missing authors. | SDKs MAY expose `report(id|pubkey, reason, text)` shorthand, but wire messages MUST use `CommonReportTarget`. ### NIP Mapping | Operation | NIP | | --- | --- | | `encodeNip19`, `decodeNip19` | NIP-19 public bech32 / TLV identifiers | | `getProfile` | NIP-01 latest kind 0 by pubkey | | `follows`, `follow`, `unfollow` | NIP-02 replacement kind 3 follow list | | `react` | NIP-25 kind 7; optional NIP-30 `emoji` tag | | `report` | NIP-56 kind 1984 | ### Wire Protocol `common.*` messages use [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format: `{ "type": "domain.action", ...payload }`. | Type | Direction | Payload fields | | --- | --- | --- | | `common.encodeNip19` | napplet -> shell | `id`, `input` | | `common.encodeNip19.result` | shell -> napplet | `id`, `ok`, `value?`, `nip19Type?`, `error?` | | `common.decodeNip19` | napplet -> shell | `id`, `value` | | `common.decodeNip19.result` | shell -> napplet | `id`, `ok`, `nip19Type?`, `hex?`, `pubkey?`, `eventId?`, `identifier?`, `relays?`, `author?`, `kind?`, `relay?`, `error?` | | `common.getProfile` | napplet -> shell | `id`, `target` | | `common.getProfile.result` | shell -> napplet | `id`, `ok`, `pubkey`, `profile?`, `result?`, `error?` | | `common.follows` | napplet -> shell | `id` | | `common.follows.result` | shell -> napplet | `id`, `ok`, `pubkeys`, `error?` | | `common.follow` | napplet -> shell | `id`, `pubkeys` | | `common.follow.result` | shell -> napplet | `id`, `ok`, `eventId?`, `event?`, `error?` | | `common.unfollow` | napplet -> shell | `id`, `pubkeys` | | `common.unfollow.result` | shell -> napplet | `id`, `ok`, `eventId?`, `event?`, `error?` | | `common.react` | napplet -> shell | `id`, `targetEventId`, `reaction`, `customEmojiHref?` | | `common.react.result` | shell -> napplet | `id`, `ok`, `eventId?`, `event?`, `error?` | | `common.report` | napplet -> shell | `id`, `target`, `reason`, `text` | | `common.report.result` | shell -> napplet | `id`, `ok`, `eventId?`, `event?`, `error?` | ### Error Handling Result messages use `ok: false` plus `error`. Common errors: `"not-signed-in"`, `"invalid-pubkey"`, `"invalid-nip19"`, `"unsupported-nip19-type"`, `"invalid-profile-target"`, `"invalid-target"`, `"invalid-reaction"`, `"invalid-report-reason"`, `"author-unresolved"`, `"user-denied"`, `"relay-timeout"`, `"publish-failed"`, `"unsupported"`. ### Shell Behavior - MUST reject NIP-19 helper requests that encode or decode `nsec`. - MUST normalize NIP-19 helper results to hex fields for keys and event ids. - MUST resolve `getProfile` targets to hex before querying relays. - MUST query kind 0 replaceable metadata for `getProfile`. - SHOULD merge observed profile-event relay URLs into `RelayEventResult.sidecar.relayHints` when the shell can disclose them. - MAY include pre-resolved profile image resources in `RelayEventResult.sidecar.resources`; NAP-RELAY's default-off sidecar privacy policy and NAP-RESOURCE's fetch policy both apply. - MUST return hex pubkeys from `follows`. - MUST perform all signing and never expose signing keys. - MUST reject modifying operations when no shell-user signer is connected; `getProfile` MAY work signed out. - MUST publish follow changes as NIP-02 replacement kind 3 events. - MUST publish reactions as kind 7 events for native Nostr event targets. - MUST publish reports as kind 1984 events with NIP-56 reasons. - MAY prompt, rate-limit, deny, redact, or route by runtime policy. ### Security Considerations - Treat NIP-19 values, relay hints, report text, custom emoji URLs, and profile metadata as untrusted. - Treat `nprofile` relays as advisory; napplets can choose hostile relays. - Prompts SHOULD show requesting napplet, action, stable targets, and report text. - Success means the shell published or returned data. It does not prove relay, client, or user acceptance. ### References - [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) — Basic protocol flow and kind 0 metadata - [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md) — Follow List - [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) — bech32-encoded entities - [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) — Reactions - [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md) — Custom Emoji - [NIP-56](https://github.com/nostr-protocol/nips/blob/master/56.md) — Reporting ### Implementations - None yet. ### Changelog - `a189372` - Introduced NAP-COMMON for shared social actions. - `8171616` - Added profile lookup to the common social action surface. - `e0b1414` - Added NIP-19 encode and decode helpers. - `f0cf652` - Renamed NIP-19 result payload `type` to `nip19Type` to avoid [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) envelope collisions. - `c52def9` - Added current-follow lookup to the common social action surface. - `7973802` - Changed common profile reads to use relay-owned result shape and declared the relay dependency. - `77ea325` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #67 ↗](https://github.com/napplet/naps/pull/67)[Read proposed changes ↗](https://github.com/napplet/naps/pull/67/files) On this pageSpecification metadata Capability domain `common` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies - [identity](https://napplet.run/protocol/nap-identity/) - [relay](https://napplet.run/protocol/nap-relay-pr-2/) - [resource](https://napplet.run/protocol/nap-resource-pr-80/) Source revision `8cd0eae85505` [View source on GitHub ↗](https://github.com/napplet/naps/pull/67/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CONFIG / PR #14 # Per-Napplet Declarative Configuration Draft PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: exploratory** Configuration binding is tested, but the upstream draft is marked MAY DEFER; use for design exploration. Implementation proposal: [draft ↗](https://github.com/napplet/naps/pull/14), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/bed9cb5b1e7c9b5babee8870aa18b12949af1d62/naps/NAP-CONFIG.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/14/files). ## NAP-CONFIG ### Per-Napplet Declarative Configuration `draft` **NAP ID:** NAP-CONFIG **Domain:** `config` **Depends:** - `storage` — layering · optional — the runtime MAY persist configuration through storage internally; this imposes no storage requirement on the napplet. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.config`; domain presence signals availability. **Parent:** [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) ### Description NAP-CONFIG provides per-napplet declarative configuration. A napplet declares a JSON Schema (draft-07 or later) describing its user-facing settings surface; the shell renders a settings UI, validates user input against the schema, persists values in a napplet-scoped store, and delivers live values back to the napplet via snapshot + push. The shell is the sole writer of configuration values. Napplets cannot mutate configuration over the wire -- they may only read (`config.get`), subscribe to live updates (`config.subscribe`), register a schema (`config.registerSchema`), and request that the shell open its settings UI (`config.openSettings`). All values a napplet sees have been validated by the shell against the currently-registered schema, with declared defaults applied. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `registerSchema` | `schema` (`ConfigSchema`), optional `version` (`uint`) | none; errors arrive through `onSchemaError` | `config.registerSchema` / `config.registerSchema.result` | | `get` | none | `ConfigValues` | `config.get` / `config.values` | | `subscribe` | handler for `ConfigValues` | `Subscription` handle | `config.subscribe` / `config.values` | | `onSchemaError` | handler for `ConfigSchemaError` | `Subscription` handle | `config.schemaError` | | `openSettings` | optional `section` (`tstr`) | none | `config.openSettings` | | `schema` | none | `ConfigSchema` or `null` | local read of currently registered schema | #### Schemas Primitive references: | Name | Meaning | | --- | --- | | `ConfigSchema` | JSON Schema draft-07 or later, restricted below. | | `ConfigValues` | Shell-validated JSON-serializable configuration values. | Enumerations: | Name | Values | | --- | --- | | `ConfigSchemaErrorCode` | `invalid-schema`, `version-conflict`, `unsupported-draft`, `ref-not-allowed`, `pattern-not-allowed`, `secret-with-default` | `ConfigSchemaError`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | `ConfigSchemaErrorCode` | yes | Machine-readable schema error code. | | `message` | `tstr` | yes | Human-readable error message. | **`registerSchema(schema, version?)`** — Declares the configuration schema through this domain. The optional `version` parameter is the `$version` migration hint. The runtime returns `config.registerSchema.result`; errors also surface via `onSchemaError`. The current napplet manifest has no configuration schema field; callers MUST register a schema before requesting values. **`get()`** -- Returns a one-shot snapshot of the current configuration values. Returned values are always validated by the shell and defaulted per the registered schema. Resolves with a `ConfigValues` object. **`subscribe(callback)`** -- Subscribes to live configuration updates. The shell MUST deliver an immediate initial `config.values` push on subscription (snapshot delivery), followed by pushes whenever the shell's settings UI commits a change. Returns a `Subscription` with a `close()` method that detaches the callback. The reference shim implementation SHOULD ref-count local subscribers and only send a wire-level `config.unsubscribe` when the last local subscriber detaches. **`onSchemaError(callback)`** -- Subscribes to schema-registration errors. The shell pushes `config.schemaError` messages when it rejects a `registerSchema` call or cannot validate a registered schema. Returns a `Subscription` with `close()`. **`openSettings(options?)`** -- Requests that the shell open its settings UI for this napplet. The optional `options.section` string deep-links the UI to a named section; the section MUST be declared via the `x-napplet-section` extension somewhere in the registered schema. Fire-and-forget -- the shell decides how to render the UI (modal, panel, new tab) and whether to honor the request. **`schema`** -- Read-only accessor for the currently-registered schema. Returns the schema last accepted through `registerSchema`, or `null` if none has been declared. Useful for napplets that render capability-dependent UI based on which settings exist. ### Identity Scope The runtime MUST derive scope from the authenticated endpoint's verified manifest and artifact. For a named manifest, the manifest key is its publisher, kind, and `d` value; for a root manifest, publisher and kind; for a snapshot, its signed event id. The scope also includes `artifactHash`, the verified single-artifact hash defined by [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303). A bare `d` value MUST NOT identify a scope. Root and snapshot manifests require no `d` tag. A napplet MUST NOT supply or override manifest or artifact identity fields in a request. Different publishers, manifest keys, and artifact hashes MUST remain isolated. ### Wire Protocol `config.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `config.registerSchema` | napplet -> shell | `id`, `schema`, `version?` | | `config.registerSchema.result` | shell -> napplet | `id`, `ok`, `error?`, `code?` | | `config.get` | napplet -> shell | `id` | | `config.subscribe` | napplet -> shell | (none) | | `config.unsubscribe` | napplet -> shell | (none) | | `config.openSettings` | napplet -> shell | `section?` | | `config.values` | shell -> napplet | `id?`, `values` | | `config.schemaError` | shell -> napplet | `error`, `code` | Key design notes: - All correlation uses `id`. - `config.values` is dual-use: with `id` it answers `config.get`; without `id` it is a subscription push. - `config.registerSchema` uses positive-ACK -- every call receives a `config.registerSchema.result` with `ok: true`, or `ok: false` accompanied by `code` and `error` describing the rejection reason. It is never error-only. - `config.subscribe` MUST produce an immediate initial `config.values` push (snapshot delivery) before any value-change pushes. - `config.unsubscribe` is the clean-teardown path; iframe unmount also implicitly unsubscribes the source. #### Examples **Register schema (success):** ``` -> { "type": "config.registerSchema", "id": "r1", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "theme": { "type": "string", "enum": ["light", "dark"], "default": "dark" } } }, "version": 1 } <- { "type": "config.registerSchema.result", "id": "r1", "ok": true } ``` **Register schema (rejection):** ``` -> { "type": "config.registerSchema", "id": "r2", "schema": { "type": "object", "properties": { "username": { "type": "string", "pattern": "^[a-z]+$" } } } } <- { "type": "config.registerSchema.result", "id": "r2", "ok": false, "code": "pattern-not-allowed", "error": "JSON Schema `pattern` keyword is not permitted in the Core Subset" } ``` **Get values:** ``` -> { "type": "config.get", "id": "g1" } <- { "type": "config.values", "id": "g1", "values": { "theme": "dark" } } ``` **Subscribe + initial snapshot:** ``` -> { "type": "config.subscribe" } <- { "type": "config.values", "values": { "theme": "dark" } } ``` **Push update after user changes a value:** ``` <- { "type": "config.values", "values": { "theme": "light" } } ``` **Unsubscribe:** ``` -> { "type": "config.unsubscribe" } ``` **Open settings with section deep-link:** ``` -> { "type": "config.openSettings", "section": "notifications" } ``` **Schema error push (background rejection):** ``` <- { "type": "config.schemaError", "code": "invalid-schema", "error": "schema root must be of type `object`" } ``` ### Schema Contract NAP-CONFIG is a JSON-Schema-driven protocol, but it is NOT all of JSON Schema. Shells and napplets MUST agree on a bounded subset so that the same schema renders and validates consistently across every conformant shell. This section defines that subset (the Core Subset), the standardized annotations shells MAY act on (Potentialities), the migration-version signal (`$version`), and the set of features explicitly excluded from v1. Any schema feature not named in Core Subset or Potentialities is out of scope for NAP-CONFIG v1. #### Core Subset Every conformant shell MUST accept and render schemas that use only the types, keywords, and constraints enumerated below. Napplets MUST NOT rely on any feature outside this subset for correct behavior. **Supported types (Core Subset):** - `object` -- top-level only. The schema root MUST be `{ "type": "object", "properties": { ... } }`. - `string` - `number` - `integer` - `boolean` - `array` of primitives -- homogeneous only. Tuple-typed arrays (`items: [schemaA, schemaB]`) are NOT in the Core Subset. - Nested `object` -- bounded depth; see Depth Limit below. Recommended maximum depth is 4. **Supported keywords (Core Subset):** - `type` - `properties` - `required` (array form) - `items` (homogeneous schema only) - `additionalProperties` (see override below) - `default` - `title` - `description` - `enum` - `enumDescriptions` -- parallel-array hint whose entries correspond positionally to `enum` entries; shells SHOULD render each description alongside its enum value. **Supported constraints (Core Subset):** - `minimum`, `maximum` -- apply to `number` and `integer` values - `minLength`, `maxLength` -- apply to `string` values - `minItems`, `maxItems` -- apply to `array` values **`additionalProperties` override:** Shells MUST treat `additionalProperties` as `false` at the top-level schema object when the napplet does not specify it explicitly. This overrides the JSON Schema draft-07 default of `true` for NAP-CONFIG scope. Rationale: persisted user settings must not accrete unknown properties silently; schema drift between napplet versions should drop orphaned keys, not preserve them. **Default-resolution rule (deterministic):** For every property at `config.values` delivery time, the shell MUST resolve its value in the following order: 1. If a persisted value exists AND validates against the current schema, use it. 2. Else if the property has its own `default`, use that. 3. Else if an ancestor `default` object supplies a value for this property, use the ancestor's value for this property. 4. Else the property is `undefined` in the delivered `config.values`. #### Standardized Extensions (Potentialities) These are opt-in annotations napplets MAY declare on schema properties. Shells MAY act on them; shells MAY ignore them. Shells that do not recognize an extension MUST treat it as opaque metadata and MUST NOT reject the schema on that basis. | Keyword | Applies to | Shell MAY | | --- | --- | --- | | `x-napplet-secret` (boolean) | any `string` property | mask input, suppress console/log output, decline to render a default value, route the stored value through a platform keychain or encrypted store | | `x-napplet-section` (string) | any property | group the property under the declared section heading in the settings UI | | `x-napplet-order` (non-negative number) | any property | sort properties within a section in ascending `x-napplet-order`; ties broken alphabetically by property key | | `deprecationMessage` (string) | any property | render the message next to the field as a deprecation notice | | `markdownDescription` (string) | any property | render the value as markdown; shells MAY fall back to plain text if markdown rendering is not available | #### `$version` Potentiality A schema MAY carry a top-level `$version` integer field. Shells MAY use `$version` to drive cross-hash migration when the napplet's `artifactHash` changes; shells MAY ignore it entirely and treat each hash as a fresh scope. NAP-CONFIG does NOT prescribe a migration strategy -- migration is a shell concern, and `$version` is a signal, not a contract. Napplets MUST NOT receive values that violate the currently-registered schema at `config.values` delivery time, regardless of how the shell reconciles older persisted values. Any clamping, dropping, or user-prompted merging happens entirely shell-side; the napplet only ever sees post-migration values. #### Exclusions (Not in v1 Core Subset) The following are explicitly excluded from NAP-CONFIG v1: - **`pattern` keyword** -- excluded due to ReDoS (Regular Expression Denial of Service) risk. JavaScript's built-in `RegExp` engine is backtracking-based; a napplet-declared pattern such as `^(a|a)*$` against a 31-character input can consume tens of seconds of CPU on the shell's main thread. See CVE-2025-69873 (ajv validator) for a concrete real-world precedent of this class of vulnerability in JSON Schema validators. Readmission requires a shell-safe regex strategy (Worker + timeout, RE2-WASM, or equivalent) to be specified in a future revision of this NAP. - **`$ref`** -- ALL forms forbidden, including same-document (`#/definitions/foo`), external URL (`https://...`), filesystem (`file://`), and cross-document relative references. Rationale: schemas are self-contained; `$ref` resolution is a security surface (exfiltration, DoS via slow URLs, TOCTOU) and a renderer-complexity explosion. - **`definitions` / `$defs`** -- follows from the `$ref` exclusion; with no `$ref` there is nothing to reference. - **`oneOf`, `anyOf`, `allOf`, `not`** -- combinatorial schemas are out of Core Subset v1. Shells MAY support them as an Extended Subset, but napplets MUST NOT rely on them for conformant behavior. - **`if` / `then` / `else`** -- conditional schemas are out of scope for v1. - **`patternProperties`, `propertyNames`, `dependencies`, `dependentSchemas`, `unevaluatedProperties`** -- draft 2019-09+ features out of scope. - **Tuple-typed arrays** (`items: [schemaA, schemaB]`) -- only homogeneous arrays are in the Core Subset. - **Recursive `$ref` of any kind** -- forbidden by the `$ref` exclusion, called out explicitly here so there is no ambiguity. - **Expression-valued defaults** (`"$now()"`, `"$random()"`, etc.) -- `default` values are literal JSON per JSON Schema semantics. Napplets needing runtime-generated defaults MUST handle the `undefined` case in their own code. - **`x-napplet-secret: true` combined with a declared `default`** -- a secret with a hardcoded default is not a secret. Shells MUST reject any schema that declares both on the same property with `code: "secret-with-default"`. #### Format as Hint Only The JSON Schema `format` keyword (`email`, `uri`, `date`, `date-time`, `color`, `ipv4`, `ipv6`, and others) is annotative in NAP-CONFIG: shells MAY use `format` to pick an input widget but MUST NOT reject a value solely because it fails the `format`. Napplets requiring strict validation MUST use `enum`, `minLength`, `maxLength`, `minimum`, or `maximum` -- NOT `format`. #### Depth Limit Shells MUST reject schemas nesting `object` properties more than 4 levels deep at `config.registerSchema` time, returning `config.registerSchema.result` with `ok: false` and `code: "schema-too-deep"`. Napplets MUST NOT assume arbitrary nesting is supported. ### Shell Guarantees The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119. #### MUST | Behavior | Details | | --- | --- | | Validate every value before delivery | Every `config.values` delivery MUST contain only values that validate against the currently-registered schema. Invalid values MUST NOT be delivered. | | Apply declared defaults | Missing properties MUST be populated from `default` per the deterministic default-resolution rule in Schema Contract. | | Use the verified identity scope | Persisted values MUST be keyed on the napplet's verified manifest and artifact scope defined in Identity Scope. A napplet cannot read or write outside its own scope. | | Be the sole writer | The shell MUST be the only entity that writes values. No napplet->shell wire message mutates persisted values. | | Mask `x-napplet-secret: true` fields (Tier 0) | Shells MUST treat properties marked `x-napplet-secret: true` as secrets at the minimum Tier 0 level: mask input in the settings UI, do NOT include the default (napplets MUST NOT set `default` on such fields), and do NOT deliver the property in `config.values` if the value has never been explicitly set by the user. | | Reject schemas exceeding the depth limit | Return `config.registerSchema.result` with `ok: false, code: "schema-too-deep"`. | | Reject schemas with forbidden features | `$ref`, `pattern` (in v1), `definitions`, combinatorial schemas (`oneOf`/`anyOf`/`allOf`/`not`), tuple-typed arrays, and conditional schemas (`if`/`then`/`else`) MUST cause `config.registerSchema.result` with `ok: false` and an appropriate code. | | Reject `x-napplet-secret: true` coexisting with `default` | Return `config.registerSchema.result` with `ok: false, code: "secret-with-default"`. | | Produce initial snapshot after registerSchema is applied | `config.subscribe` MUST NOT deliver its first `config.values` until the most recent `config.registerSchema` from the same source has been fully applied (defaults resolved, storage scoped). If subscribe arrives before any schema has been registered (no accepted registerSchema), shells MUST emit `config.schemaError` with `code: "no-schema"`. | | Drop orphaned properties | Persisted values for properties not in the currently-registered schema MUST NOT be delivered. For `x-napplet-secret: true` properties, orphaned values MUST be deleted immediately on schema change; non-secret orphans MAY be retained briefly (grace period) but MUST NOT be delivered. | #### SHOULD | Behavior | Details | | --- | --- | | Group by `x-napplet-section` | Shells SHOULD render properties grouped under the declared section heading. | | Sort within a section by `x-napplet-order` | Ascending; ties broken by property-key alphabetical order. Properties without `x-napplet-order` come after, alphabetically sorted. | | Surface `deprecationMessage` | Display next to affected fields so users understand impending removal. | | Render `markdownDescription` as markdown | Falling back to plain text. Never render as HTML. | | Coalesce rapid value changes | Debounce user-side edits (recommend ~100ms) so napplets receive terminal values, not every keystroke. | | Drop non-secret orphans after a grace period | One session is the recommended grace window. | | Honor `config.openSettings` for focused napplets | Only when the calling napplet has (or would have) user focus. Rate-limit repeated requests. | #### MAY | Behavior | Details | | --- | --- | | Tier 2+ secret handling | Shells MAY store `x-napplet-secret: true` values encrypted at rest, in an OS keychain, or in another hardened backend. NOT required -- a Tier 0 in-process store is conformant. | | Render richer `format` widgets | For `format: "email" | "uri" | "date" | "date-time" | "color" | "ipv4" | "ipv6"`, shells MAY render a specialized widget; MUST NOT fail validation on `format` alone. | | Render nested objects beyond one level | Up to the depth limit (4). JSON fallback UI is acceptable for deep nesting. | | Back NAP-CONFIG storage with NAP-STORAGE internally | An implementation choice. The NAP-CONFIG wire surface does not depend on NAP-STORAGE. | | Use `$version` for cross-hash migration | Shells MAY act on `$version` to migrate values across `artifactHash` changes; MAY ignore it and treat each hash as a fresh scope. | | Retain a "graveyard" of orphaned non-secret values | For a single session in case the napplet author rolls back a schema change. Graveyard values MUST NOT be delivered. | | Emit `config.settingsOpened` after openSettings | Optional ack so napplets can fall back when UI is unavailable. Napplets MUST NOT rely on this message. | ### Anti-Features The following ideas are deliberately excluded from NAP-CONFIG and MUST NOT appear in conformant implementations. They are listed here so that proposals to add them surface the prior rationale rather than re-litigating. - **`config.set` (napplet->shell write message)** -- violates the sole-writer invariant; defeats shell-UI validation and consent UX; enables silent settings override by compromised napplets. - **`$ref` / `definitions` / `$defs` in schemas** -- schemas are self-contained; $ref resolution is a security surface (exfiltration, DoS, TOCTOU) and a renderer complexity explosion. Bundle refs at build time. - **`pattern` keyword in Core Subset v1** -- ReDoS exposure per CVE-2025-69873 class. Readmission requires a shell-safe regex strategy in a future revision. - **napplet-rendered settings iframe** -- defeats consistent UX across napplets and places the settings surface inside the same sandbox that renders the napplet. - **napplet-supplied validation code** -- the shell executing napplet code is a sandbox violation and breaks validate-before-delivery. - **napplet-dictated UI widget hints (`ui:widget`)** -- turns NAP-CONFIG into a UI framework spec. - **Migration-as-napplet-code** -- shell is the risk carrier; `$version` as a signal suffices. - **Cross-napplet config sharing** -- violates per-napplet isolation (security property). - **Live two-way binding / streaming uncommitted keystrokes** -- creates validation flicker and storage churn; shells debounce and push on commit instead. - **JSON Schema draft 2020-12 conditional features** (`if`/`then`/`else`, `unevaluatedProperties`, `dependentSchemas`, `$dynamicRef`) -- enormous validator/UI complexity, no precedent uses them for extension config. - **OS-keychain secret handling as MUST** -- unimplementable in browser-only shells; kept as MAY. ### Security Considerations #### Source-identity scope binding Storage scope is derived from the authenticated endpoint's verified manifest and artifact, as specified in Identity Scope. NAP-CONFIG requests MUST NOT carry caller-selected identity or scope fields. A napplet cannot read, register, or mutate another napplet's configuration scope. #### Cleartext secrets over postMessage `config.values` payloads are transmitted cleartext over `postMessage` to the napplet iframe, including fields marked `x-napplet-secret: true`. This is a property of the sandbox model (no secure key-exchange channel exists between a sandboxed iframe and its host that isn't itself `postMessage`). Implementers MUST be aware that: - Browser extensions with script access can observe the event - Developer-tools inspection exposes the payload - Crash reporters and analytics may capture it Shells SHOULD suppress console logging of `config.values` deliveries containing `x-napplet-secret: true` fields. Napplets SHOULD read secrets on demand and avoid holding them in long-lived memory. NAP-CONFIG does NOT claim secrets are "secure" -- the honest framing is that the shell decides when to deliver. #### `additionalProperties: false` override JSON Schema draft-07 defaults `additionalProperties` to `true`. NAP-CONFIG overrides this: the top-level schema object is treated as if `additionalProperties: false` when the napplet does not specify otherwise. This prevents silent persisted-data accretion and makes schema evolution deterministic (orphaned properties drop out of delivery). Napplets SHOULD still declare `additionalProperties: false` explicitly for robustness across shell implementations. #### External `$ref` forbidden All `$ref` forms are forbidden in v1 (see Schema Contract Exclusions). The ban is particularly important for external references (`http://`, `https://`, `file://`, `ftp://`): they enable data exfiltration (the request URL can carry stolen values), denial-of-service (slow or hanging URLs stall validation), and privacy leaks (every validation pings the remote). Shells MUST reject any schema containing `$ref` at `config.registerSchema` time. ### Error Envelopes Every napplet->shell request type either returns a correlated result message with an `ok: boolean` field (positive ACK) or fires a matching push error. Result messages follow the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) envelope format `{ "type": "domain.action.result", "id", "ok", "code"?, "error"? }`. Error `code` values are drawn from the catalogue below; `error` is a human-readable explanation. #### Error codes | Code | Emitted by | Meaning | | --- | --- | --- | | `invalid-schema` | `config.registerSchema.result`, `config.schemaError` | Napplet-supplied schema is not parseable as JSON Schema draft-07 or later. | | `unsupported-draft` | `config.registerSchema.result`, `config.schemaError` | Schema declares a JSON Schema draft the shell does not support. | | `ref-not-allowed` | `config.registerSchema.result`, `config.schemaError` | Schema uses `$ref`, `definitions`, or `$defs`. | | `pattern-not-allowed` | `config.registerSchema.result`, `config.schemaError` | Schema uses the `pattern` keyword (excluded in Core Subset v1). | | `secret-with-default` | `config.registerSchema.result`, `config.schemaError` | A property marked `x-napplet-secret: true` declares a `default`. | | `schema-too-deep` | `config.registerSchema.result`, `config.schemaError` | Schema nesting exceeds the depth limit (4). | | `version-conflict` | `config.registerSchema.result`, `config.schemaError` | Re-registration declares a `$version` the shell does not accept. | | `no-schema` | `config.schemaError` | `config.subscribe` or `config.get` received before any schema has been registered for this source (no accepted registerSchema). | | `unknown-section` | (see below -- non-normative) | `config.openSettings` referenced a section that does not exist on the currently-registered schema. SHOULD NOT be surfaced as an error envelope; shells SHOULD silently ignore (see Shell Guarantees SHOULD row for openSettings). | #### Error cases **Malformed schema at registerSchema time.** A schema that is not parseable, declares an unsupported draft, uses forbidden features, or violates the depth limit is rejected. The shell emits `config.registerSchema.result` with `ok: false` and the appropriate `code`. Napplets that registered via `window.napplet.config.registerSchema()` observe the failure via the `onSchemaError` listener; the shim correlates the `.result` via `id`. **Undeclared section in openSettings.** When `config.openSettings({ section })` references a `section` not declared by any property's `x-napplet-section` in the currently-registered schema, shells SHOULD silently ignore the request (opening the settings UI to its default entry is acceptable). Shells MUST NOT return a wire error for this case -- that would leak shell internals. The section string is case-sensitive and compared as an opaque identifier. **Subscribe-before-schema.** `config.subscribe` or `config.get` that arrives before any schema has been registered (no accepted `config.registerSchema`) receives a `config.schemaError` push with `code: "no-schema"`. No `config.values` is emitted until a valid schema has been registered. ### Implementations - (none yet) ### Changelog - `8d671ab` - Introduced NAP-CONFIG for per-napplet declarative configuration. - `36c209b` - Adopted injected-domain availability and verified manifest/artifact isolation, and required registerSchema instead of an undefined manifest schema field. - `0246251` - Declared optional internal storage layering without imposing a storage capability requirement on napplets. [Join the discussion · PR #14 ↗](https://github.com/napplet/naps/pull/14)[Read proposed changes ↗](https://github.com/napplet/naps/pull/14/files) On this pageSpecification metadata Capability domain `config` Repository state Draft PR Registry status Draft Document marker draft Declared dependencies - [storage](https://napplet.run/protocol/nap-storage-pr-3/) Source revision `bed9cb5b1e7c` [View source on GitHub ↗](https://github.com/napplet/naps/pull/14/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CONNECT / PR #19 # User-Gated Direct Network Access Draft PRDeferred upstream The registry marks NAP-CONNECT as deferred. This proposal is dormant and should not be treated as an available protocol capability. This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/ec81a3a4a1fc4da1ad2601215353b7f822e44b8f/naps/NAP-CONNECT.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/19/files). ## NAP-CONNECT ### User-Gated Direct Network Access `draft` **NAP ID:** NAP-CONNECT **Domain:** `connect` **Web binding (NIP-5D):** `window.napplet.connect` · `shell.supports("connect")` **Parent:** NIP-5D ### Description NAP-CONNECT provides sandboxed napplets with user-gated direct network access (`fetch`, `WebSocket`, `SSE`, `EventSource`) to a pre-declared list of origins. Origins are declared at build time via `["connect", ""]` tags in the NIP-5A manifest. The host shell reads those tags, prompts the user for approval at first load of a given `(dTag, aggregateHash)`, persists the decision, and emits a runtime Content Security Policy whose `connect-src` directive contains the approved origin list. The user's decision is expressed through the HTTP-response CSP the shell serves with the napplet's HTML — not through any postMessage traffic. A napplet that declares any `connect` tag takes on the posture defined in `NAP-CLASS-2.md`. A napplet that declares no `connect` tags takes on the default posture defined in `NAP-CLASS-1.md`. This NAP does not redefine those class postures — see `NAP-CLASS-1.md` and `NAP-CLASS-2.md` for CSP shapes, consent-flow MUSTs, grant-persistence semantics, residual-meta-CSP refusal requirements, and revocation responsibilities. NAP-CONNECT defines the manifest-tag shape, origin format, canonical aggregateHash fold, runtime API, and capability advertisement that feed into the class determination; the postures themselves are owned by the sub-track documents. NAP-CONNECT has **no postMessage wire protocol**. Grants are expressed entirely through the runtime CSP the shell emits in the HTTP response for the napplet's HTML, plus a shell-injected discovery meta tag (``) read synchronously by the napplet shim at install time. There are no `connect.*` envelopes in either direction. Implementers and readers looking for a wire message in this spec will not find one — the absence is deliberate. ### Motivation The sandbox model inherited from NIP-5D (`sandbox="allow-scripts"`, no `allow-same-origin`) combined with a restrictive baseline CSP means napplets cannot, by default, reach the network directly. NAP-RESOURCE partially fills the gap with a shell-mediated single-`Blob` primitive (`resource.bytes(url) → Blob`): read-only, shell-visible, policy-enforced at the URL layer, with MIME byte-sniffing and SVG rasterization baked in. That primitive covers avatars, static assets, one-shot byte fetches, and bech32 resolution — a large fraction of napplet use cases. What NAP-RESOURCE cannot express: `POST` / `PUT` / `PATCH` methods, WebSocket, Server-Sent Events, streaming responses, custom request headers, long-lived connections, and any third-party library that calls `fetch` or opens a socket directly. For those use cases, the napplet needs direct browser-level network access to a specific known host. Direct network access in a sandboxed iframe requires explicit CSP relaxation, which requires user consent — because the shell cannot see or filter traffic that flows directly from the iframe to an approved origin. NAP-CONNECT is the minimum-viable protocol for that consent. Authors SHOULD default to NAP-RESOURCE for everything it can express; NAP-CONNECT exists specifically for the cases where the resource NAP cannot. The two NAPs are complementary, not competing — most non-trivial napplets will use both. ### Non-Goals - **Per-origin partial grants.** v1 is all-or-nothing: approving the prompt approves every declared origin. A future revision MAY add partial grants. - **Wildcard subdomains.** `https://*.example.com` is not supported. Each subdomain is a separate origin requiring its own `connect` tag and its own line in the consent prompt. - **Quota or rate-limiting on granted traffic.** The browser provides no hook for a shell running in a parent frame to inspect or rate-limit traffic from a sandboxed child frame's direct network I/O. A service worker could mediate the traffic, but the napplet's `sandbox="allow-scripts"` iframe forbids service-worker registration. - **Audit logging of individual network calls.** Same reason: the browser enforces CSP transparently to the shell. The shell sees no per-request trace. - **A postMessage wire protocol.** NAP-CONNECT is expressed through manifest tags, CSP, and a discovery meta tag. No `connect.request`, no `connect.approve`, no `connect.granted` envelope exists. Adding one would contradict the architectural decision documented here and in NAP-CLASS-2.md. - **Shell visibility into post-grant traffic.** Once the user approves an origin, the shell has no browser-level mechanism to observe, filter, or intercept subsequent traffic between the napplet and that origin. This is the fundamental tradeoff of NAP-CONNECT versus NAP-RESOURCE; it is not a defect to be mitigated but a property to be disclosed clearly in the consent prompt. ### Architecture Overview Three moving parts, and nothing else: - **Napplet build.** The napplet author declares required origins through the napplet build tool's `connect: string[]` option. The build tool normalizes each origin (lowercase scheme and host, Punycode for IDN, strip default ports, reject malformed input), emits one `["connect", ""]` tag per origin into the signed NIP-5A manifest, and folds the normalized origin set into `aggregateHash` via a synthetic `connect:origins` xTag entry. Any change to the origin set — addition, removal, reorder after normalization — produces a different `aggregateHash` and therefore a different grant key. - **Shell runtime.** The shell reads the manifest's `connect` tags, validates each origin against the format rules, and looks up grant state keyed on `(dTag, aggregateHash)`. On first load of a new key: prompt the user, persist the decision. On subsequent loads: reuse the persisted decision. On approval, emit a runtime CSP whose `connect-src` directive contains the approved origins and inject `` into the served HTML. On denial, fall back to the posture defined in `NAP-CLASS-1.md` (the shell emits `connect-src 'none'` and the injected meta tag is empty or absent). Grant-persistence and consent-flow details are owned by `NAP-CLASS-2.md`. - **Napplet runtime.** The napplet shim reads the discovery meta tag synchronously at install time and populates `window.napplet.connect` with `{ granted, origins }`. Napplet code branches on `window.napplet.connect.granted`: when `true`, call `fetch` / open `WebSocket` / `EventSource` directly against approved origins; when `false`, fall back to NAP-RESOURCE for what the resource NAP can express, or degrade the affected feature gracefully. The shim never waits for a wire message — grant state is known at install time. ### Posture Citation A napplet declaring any `["connect", ""]` manifest tag takes on the posture defined in `NAP-CLASS-2.md`. A napplet with no `connect` tags takes on the default posture defined in `NAP-CLASS-1.md`. NAP-CONNECT does NOT redefine those postures: this NAP specifies only the manifest-tag shape, the origin format, the canonical aggregateHash fold, the runtime API, and the capability advertisement that feed the class determination. The concrete CSP directive shapes, the consent-prompt MUSTs, the shell responsibilities at serve time, the grant-persistence semantics, the residual-meta-CSP refuse-to-serve requirement, and the revocation UX are owned by `NAP-CLASS-2.md` — readers looking for any of those details MUST follow the citation rather than treating this section as a substitute. For the default-deny posture that a napplet without `connect` tags receives (and that a napplet whose user denied the consent prompt is downgraded to), see `NAP-CLASS-1.md`. ### Manifest Tag Shape Origins are declared in the NIP-5A manifest using one tag per origin: ``` ["connect", ""] ``` The `` value MUST be the lowercase, normalized, Punycode-encoded form defined in **Origin Format** below. Tags are signed as part of the NIP-5A manifest and participate in `aggregateHash` via a synthetic xTag entry defined in **Canonical `connect:origins` aggregateHash Fold** below. One origin, one tag; an origin list of length N produces exactly N `connect` tags in the manifest. Worked example: a manifest fragment declaring three origins (two ASCII, one IDN converted to Punycode): ``` ["connect", "https://api.example.com"] ["connect", "wss://events.example.com"] ["connect", "https://xn--caf-dma.example.com"] ``` The napplet-build tool is the sole author of these tags. Napplet authors do NOT hand-edit the manifest; they supply origins to the build tool's `connect: string[]` option and the build tool performs normalization, validation, and emission. ### Origin Format Each origin MUST conform to the following rules. The napplet-build tool MUST reject any origin that fails these rules at build time; the shell MUST reject any `connect` tag whose value fails these rules at manifest-load time. Both sides enforce because the build rejection catches author errors early and the shell rejection catches tampering after signing. - **Scheme whitelist.** The scheme part MUST be one of `https:`, `wss:`, `http:`, or `ws:`. Any other scheme — including `file:`, `ftp:`, `gopher:`, `data:`, `blob:`, and custom schemes — MUST be rejected. This is a closed set; shells MUST NOT accept additional schemes under any policy, and the build tool MUST NOT emit them. - **Lowercase scheme and host.** The scheme and host parts MUST be lowercase on the wire. `HTTPS://FOO.COM` is invalid as emitted. The build tool MUST lowercase before emission; the shell MUST reject non-lowercase input with a diagnostic. Shells MUST NOT silently lowercase on lookup — doing so would cause two cosmetically different origins to produce two different aggregateHashes while matching the same CSP directive, breaking the determinism rationale for the fold below. - **Punycode IDN.** A host containing any non-ASCII character MUST be Punycode-encoded (`xn--` form, lowercase) before emission. The build tool is the sole authority for the UTF-8 → Punycode conversion. The shell MUST NOT auto-convert and MUST reject any non-Punycode non-ASCII host with a diagnostic. This direction of authority ensures that whatever Punycode form the napplet author signs into the manifest is the byte-identical form the shell sees on lookup. - **No wildcards.** The host MUST NOT contain `*`. Wildcard subdomains (for example `https://*.example.com`) MUST be rejected at build time and at manifest-load time. Each subdomain is a separate origin that requires its own `connect` tag and participates independently in the consent prompt. - **Default ports banned.** If the port component would be the scheme's default (443 for `https:` and `wss:`, 80 for `http:` and `ws:`), the port MUST be omitted from the origin. `https://foo.com:443` MUST be rejected at build time; `https://foo.com` is the correct form. The rationale is aggregateHash determinism: two cosmetically different forms that CSP treats as equivalent origin-matches would otherwise produce different hashes, silently invalidating prior grants on no semantic change. Shells MUST NOT normalize default ports on lookup for the same reason. - **No path, query, or fragment.** The origin is scheme + host + optional non-default port. `https://foo.com/api`, `https://foo.com?x=1`, and `https://foo.com#section` are all invalid. CSP `connect-src` matches at the origin granularity; path-level constraints are out of scope for NAP-CONNECT. - **No trailing slash.** The origin MUST NOT end in `/`. `https://foo.com/` is invalid; `https://foo.com` is the correct form. This rule is a consequence of the no-path rule but is worth calling out because many URL-parsing libraries auto-append a trailing slash when stringifying a parsed origin. #### Origin Normalization Conformance Vectors The table below enumerates canonical input/output pairs a conformant build tool and a conformant shell MUST agree on. Rows marked `reject` indicate inputs that MUST be refused with a diagnostic; rows with a normalized output indicate inputs that are accepted after producing the specified byte-identical string. | Input | Normalized | Reason | | --- | --- | --- | | `HTTPS://api.example.com` | `https://api.example.com` | Uppercase scheme lowercased by build tool | | `https://API.example.com` | `https://api.example.com` | Uppercase host lowercased by build tool | | `https://foo.com/` | *reject* | Trailing slash forbidden | | `https://foo.com:443` | *reject* | Default port `:443` for `https:` MUST NOT be written | | `wss://events.example.com:443` | *reject* | Default port `:443` for `wss:` MUST NOT be written | | `http://foo.com:80` | *reject* | Default port `:80` for `http:` MUST NOT be written | | `https://api.example.com:8443` | `https://api.example.com:8443` | Non-default port accepted verbatim | | `https://*.example.com` | *reject* | Wildcard host forbidden | | `https://foo.com/api` | *reject* | Path not permitted in origin | | `https://foo.com?x=1` | *reject* | Query not permitted in origin | | `https://foo.com#top` | *reject* | Fragment not permitted in origin | | `ftp://files.example.com` | *reject* | Scheme not in whitelist | | `data:text/plain,hello` | *reject* | Scheme not in whitelist | | `https://café.example.com` | `https://xn--caf-dma.example.com` | UTF-8 IDN converted to Punycode by build tool | | `https://xn--caf-dma.example.com` | `https://xn--caf-dma.example.com` | Already Punycode, accepted verbatim | | `https://CAFÉ.example.com` | *reject* | Build tool rejects uppercase non-ASCII; author MUST supply lowercase or ASCII | | `https://日本.example.com` | `https://xn--wgv71a.example.com` | Multi-byte UTF-8 IDN converted to Punycode by build tool | | `https://api.example.com` | `https://api.example.com` | Already canonical, accepted verbatim (VALID accept case) | Shell implementers MUST produce byte-identical output to the build tool for every accepted row. A disagreement on any row is a conformance bug. ### Canonical `connect:origins` aggregateHash Fold The set of accepted origins MUST be folded into `aggregateHash` via a synthetic xTag entry so that any change to the origin set invalidates prior grants keyed on `(dTag, aggregateHash)`. The fold is defined as normative pseudocode; the output is a 64-character lowercase hexadecimal SHA-256 digest. ``` # Input: origins = list[str] # Each origin has already been validated and normalized per the # Origin Format rules above (scheme lowercased, host lowercased, # Punycode-encoded, no default port, no path / query / fragment, # no trailing slash). # Output: a 64-character lowercase hexadecimal SHA-256 digest. def connect_origins_fold(origins: list[str]) -> str: normalized = [o.lower() for o in origins] # 1. lowercase (idempotent after normalization) normalized.sort() # 2. ASCII-ascending sort joined = "\n".join(normalized) # 3. join with single LF, NO trailing newline encoded = joined.encode("utf-8") # 4. UTF-8 encode digest = sha256(encoded).hexdigest() # 5. SHA-256 return digest.lower() # 6. lowercase hex (already lowercase from hexdigest) ``` The resulting digest is pushed as a synthetic xTag entry `[, 'connect:origins']` into the manifest's xTag array **before** `aggregateHash` is computed. This synthetic entry participates in `aggregateHash` but is filtered out of the `['x', ...]` tag projection in the signed manifest — the public manifest surfaces origins as their own `['connect', ...]` tags, not as pseudo-file x-tag entries. Shells performing grant lookups MUST recompute the fold from the manifest's `['connect', ...]` tags (after normalization-equivalence verification) and MUST verify that the recomputed digest matches the synthetic `connect:origins` xTag's hash. A mismatch MUST result in manifest rejection with a diagnostic; a mismatch indicates either a build-tool bug or post-signing tampering, and in neither case is it safe to proceed. This double-entry — origin tags plus folded hash — is the guard against silent origin-list tampering. #### Conformance Fixture The following fixture is normative for interop. A conformant build tool and a conformant shell MUST both produce the specified hexadecimal digest from the specified input origins. ``` Input origins (post-normalization, as supplied by the napplet author): https://api.example.com https://xn--caf-dma.example.com wss://events.example.com Sorted (ASCII-ascending): https://api.example.com https://xn--caf-dma.example.com wss://events.example.com Joined with literal LF (\n), no trailing newline: https://api.example.com\nhttps://xn--caf-dma.example.com\nwss://events.example.com UTF-8 byte length of the joined string: 80 Expected SHA-256 lowercase hex digest: cc7c1b1903fb23ecb909d2427e1dccd7d398a5c63dd65160edb0bb8b231aa742 ``` The digest above can be verified independently with a one-liner such as `python3 -c "import hashlib; print(hashlib.sha256(b'https://api.example.com\nhttps://xn--caf-dma.example.com\nwss://events.example.com').hexdigest())"`. Implementations whose output differs from `cc7c1b1903fb23ecb909d2427e1dccd7d398a5c63dd65160edb0bb8b231aa742` for the specified inputs are non-conformant. ### Runtime API The napplet-side runtime surface is a single property `connect` mounted at `window.napplet.connect`: `NappletConnect`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `granted` | `bool` | yes | True when all declared origins are granted for this `(dTag, aggregateHash)`. | | `origins` | list of `tstr` | yes | User-approved origins; empty on denial or unsupported shells. | The shim populates `window.napplet.connect` synchronously at install time from a shell-injected `` element in the served HTML. The meta tag's `content` attribute is a space-separated list of approved origins. An empty `content` value or an absent meta tag indicates `{ granted: false, origins: [] }`. Shells implementing this NAP MUST inject the discovery meta tag when serving the napplet's HTML — even on denial, in which case the tag is emitted with an empty `content` attribute. The presence of the meta tag is the signal that the shell implements NAP-CONNECT; absence is indistinguishable from a shell that does not advertise `nap:connect`. Shells MUST NOT rely on postMessage traffic to convey grant state; `window.napplet.connect` is populated before any napplet code runs. `window.napplet.connect` MUST default to `{ granted: false, origins: [] }` on shells that do not implement `nap:connect`, on shells that implement `nap:connect` but denied the prompt, and on shells that have not yet injected the meta tag at shim install. The property MUST NEVER be `undefined`. Napplets gracefully degrade by checking `shell.supports('nap:connect')` before depending on `window.napplet.connect.granted === true`; a napplet that branches solely on `.granted` without checking `shell.supports(...)` will produce correct (degraded) behavior but will not be able to distinguish "shell does not implement the NAP" from "user denied the prompt". The `NappletConnect` object and its `origins` array SHOULD be frozen via `Object.freeze` at shim install so that napplet code cannot tamper with the state after install. Freezing is a best-effort integrity signal, not a security control — the napplet runs in the same execution context as the shim and can always replace `window.napplet.connect` wholesale. The shell's CSP header, not the shim object, is the authoritative enforcement. ### Capability Advertisement Shells advertise NAP-CONNECT support through three capability strings on the NIP-5D `shell.supports()` surface: - `shell.supports('nap:connect')` — primary capability. Returns `true` when the shell implements this NAP: honors `connect` manifest tags, injects the discovery meta tag, performs the origin-format validation and the `(dTag, aggregateHash)` grant-persistence model, and emits the runtime CSP described in `NAP-CLASS-2.md`. A napplet that branches on `.granted` SHOULD first check this capability to distinguish "shell denied" from "shell does not implement". - `shell.supports('connect:scheme:http')` — secondary, operator policy. Returns `true` if the shell permits cleartext `http:` origins in `connect` tags. Operator policy MAY refuse cleartext entirely — some deployments require HTTPS end-to-end and advertise `false` for this capability. - `shell.supports('connect:scheme:ws')` — secondary, parallel to `connect:scheme:http`. Returns `true` if the shell permits cleartext `ws:` origins. Napplets declaring cleartext (`http:` or `ws:`) origins SHOULD check the corresponding `connect:scheme:*` capability before relying on a grant being obtainable and SHOULD gracefully degrade when the shell refuses cleartext. When a shell refuses to serve a napplet solely on the basis of a cleartext origin, the refuse-to-serve diagnostic MUST identify the offending origin and the operator-policy reason, so that the napplet author or a sophisticated end user can understand why the napplet cannot load. ### Shell Consent Flow The shell consent flow is owned by the posture defined in `NAP-CLASS-2.md`. NAP-CONNECT specifies only the triggering condition and the semantic content of the prompt; the UX wording, layout, and revocation machinery are posture concerns. The trigger for the consent flow is: the napplet's manifest contains at least one `['connect', '']` tag AND no prior approve-or-deny decision exists for the composite `(dTag, aggregateHash)` key. Shells encountering this condition MUST present a consent prompt before the iframe is served; shells MUST NOT silently approve or silently deny. The prompt MUST capture the following semantic elements. Exact wording is a shell-UX decision, but these MUSTs are load-bearing regardless of copy: 1. Napplet name as declared in the NIP-5A manifest. 2. The complete list of requested origins (verbatim as declared in the `connect` tags — shells MUST NOT summarize, truncate, or first-presentation-elide the list). 3. An explicit statement that approval allows the napplet to **send AND receive** any data with the listed origins — the grant is not read-only and is not shell-mediated. 4. An explicit statement that the shell cannot see or filter post-grant traffic between the napplet and the listed origins. 5. Visible marking of any cleartext (`http:` or `ws:`) origins alongside the confidentiality tradeoff: the user is approving traffic that is not encrypted in transit. Shells MUST NOT use diminutive language ('just', 'only', 'simply') that understates the trust cost of approval. Consent prompts written to minimize user friction at the cost of informed consent are misleading the user. See `NAP-CLASS-2.md` for the complete consent-flow MUSTs — including refuse-to-serve behavior on residual meta-CSP, revocation UX, at-next-load-only effective-revocation timing, and the full set of shell-side responsibilities. ### Grant Persistence Grants are keyed on the composite `(dTag, aggregateHash)`. The `aggregateHash` input includes the `connect:origins` synthetic xTag entry defined in the **Canonical `connect:origins` aggregateHash Fold** section above. Any change to the napplet's origin set (addition, removal, or reorder after normalization) produces a new `aggregateHash`, which auto-invalidates the prior grant and triggers a fresh consent prompt on next load. Shells MUST key grants on the exact composite. Keying on `dTag` alone is a security bug: a rebuilt napplet with a changed origin set would inherit the prior approval silently, giving the attacker-in-the-rebuild a free pass to reach newly-added origins the user never approved. This is the silent-supply-chain-upgrade vector, and the composite-key rule is the direct mitigation. See `NAP-CLASS-2.md`'s Grant Persistence Semantics section for the full posture-level MUSTs — including the diff UX requirement on re-prompt when a prior approved grant existed under the same `dTag` but a different `aggregateHash`, and the extended-key hygiene rule (shells MAY extend the key tuple but the extension MUST be a strict superset of `(dTag, aggregateHash)` to preserve portability across conformant shells). ### Security Considerations #### Post-grant opacity Once origins are approved and the runtime CSP is emitted, the shell has zero browser-level hook to inspect or filter subsequent network traffic between the napplet and the approved origins. This is a fundamental tradeoff and cannot be papered over. Consent-prompt language MUST reflect this reality — see **Shell Consent Flow** above. Mechanisms that would enable post-grant traffic inspection — audit logging, per-request quotas, traffic filtering — all require a service worker intercepting the napplet's network layer, and the napplet's `sandbox="allow-scripts"` iframe forbids service-worker registration. NAP-CONNECT does not attempt to provide what the browser's security model does not allow. #### Mixed-content silent failure Browsers block `http:` and `ws:` fetches from napplets running in shells served over `https:`, regardless of the CSP header's `connect-src` value. A napplet declaring `http:` origins approved by the user will silently fail to fetch when the shell is served over `https:`, except for the `localhost` / `127.0.0.1` secure-context exceptions. The napplet-build tool SHOULD emit a build-time warning when `http:` or `ws:` origins appear in `connect`; shells SHOULD surface this reality in the consent prompt so the user is not surprised by an apparently-approved grant that produces no traffic in practice. Mixed-content is a browser-level rule enforced below the CSP layer; NAP-CONNECT cannot override it. #### Sandbox preservation Approving `connect` origins does NOT relax the iframe sandbox. The napplet remains at an opaque origin under `sandbox="allow-scripts"`. Grants relax only the `connect-src` CSP directive; all other browser protections (Same-Origin Policy against the shell's origin, absence of cookies on napplet-origin requests, no service-worker registration, no shared-storage access, no direct access to the shell's DOM) remain in force. A NAP-CONNECT grant is a network-egress permission scoped to specific origins — it is not a sandbox escape, it is not a capability-expansion beyond network, and it does not interact with NAP-RESOURCE's own enforcement layer. #### Weaker posture than NAP-RESOURCE NAP-CONNECT is substantially weaker than NAP-RESOURCE in terms of shell-side visibility and policy enforcement. NAP-RESOURCE proxies all fetches through the shell, which can MIME-sniff response bytes, enforce a private-IP block list at DNS-resolution time, rasterize SVG in a sandboxed worker, cap response size, limit redirect chains, and re-validate per hop. NAP-CONNECT does none of those things — the grant is a full trust vote for the listed origins, and every byte flowing through a NAP-CONNECT grant is outside the shell's view. Napplet authors SHOULD default to NAP-RESOURCE for everything NAP-RESOURCE can express, and reach for NAP-CONNECT only when the use case genuinely requires direct `POST`, WebSocket, SSE, streaming responses, custom headers, or long-lived connections. ### Graceful Degradation Napplets that might want network access SHOULD branch on four states, in priority order: 1. `shell.supports('nap:connect') === true` AND `window.napplet.connect.granted === true` — the user approved the manifest's `connect` origins. Use direct `fetch` / `WebSocket` / `EventSource` against `window.napplet.connect.origins`. 2. `shell.supports('nap:connect') === true` AND `window.napplet.connect.granted === false` — user denied the prompt, the prompt has not yet been presented, or the shell implements the NAP but chose not to advertise grants for this napplet. Fall back to NAP-RESOURCE for anything the resource NAP can express; otherwise degrade the affected feature gracefully. Optionally display a napplet-side affordance prompting the user to re-approve through the shell's revocation UI. 3. `shell.supports('nap:connect') === false` AND `shell.supports('nap:resource') === true` — shell does not implement NAP-CONNECT at all; use NAP-RESOURCE for read-only byte fetches. POST / WebSocket / SSE features are unavailable in this environment and MUST be degraded gracefully; the napplet MUST NOT pressure the user to switch shells. 4. Neither `nap:connect` nor `nap:resource` is advertised — the napplet cannot reach the network at all. Display appropriate UX (offline mode, read-only-from-cache, or a clear explanation that the feature requires a shell implementing one of the network NAPs) and avoid silent failure. States (1) and (2) are the common paths in modern shells. States (3) and (4) are graceful-shutdown paths that napplets targeting restrictive or minimal shells MUST handle without crashing, hanging, or presenting confusing error messages. ### References - `NIP-5D` — Parent transport spec: JSON envelope wire format, iframe sandbox model, capability advertisement surface. - `NAP-CLASS.md` — Parent class track: defines the `class.assigned` envelope, the `window.napplet.class` runtime surface, and the authoring rules for track members. - `NAP-CLASS-1.md` — Default strict-baseline posture. Triggered when a napplet declares no `connect` tags, and also when a user denies the NAP-CLASS-2 consent prompt (denied napplets are served under the NAP-CLASS-1 posture). - `NAP-CLASS-2.md` — User-approved explicit-origin posture triggered by presence of `connect` tags. Owns the CSP shape, consent-flow MUSTs, grant-persistence semantics, residual-meta-CSP refuse-to-serve requirement, and revocation UX. - `NAP-RESOURCE` — Sibling NAP providing shell-mediated read-only byte fetching. Authors SHOULD default to NAP-RESOURCE for everything it can express and reach for NAP-CONNECT only when direct network access is genuinely required. - WHATWG URL — Origin format and Punycode IDN conversion rules. - WHATWG Fetch, Mixed Content — Browser-level mixed-content enforcement (HTTPS shells cannot fetch HTTP origins; `localhost` / `127.0.0.1` secure-context exceptions). ### Changelog - `fdf6c3e` - Introduced NAP-CONNECT for user-gated direct network access. [Join the discussion · PR #19 ↗](https://github.com/napplet/naps/pull/19)[Read proposed changes ↗](https://github.com/napplet/naps/pull/19/files) On this pageSpecification metadata Capability domain `connect` Repository state Draft PR Registry status Deferred Document marker draft Declared dependencies None declared in the preamble Source revision `ec81a3a4a1fc` [View source on GitHub ↗](https://github.com/napplet/naps/pull/19/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-COUNT / PR #69 # Runtime-Mediated Event Counts Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** Count binding is tested, but accuracy and aggregation across real relay backends remain outside that evidence. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/69), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/9353a0c6c02c595f7e65ebd6df3802a7a45fc6bf/naps/NAP-COUNT.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/69/files). ## NAP-COUNT ### Runtime-Mediated Event Counts `draft` **NAP ID:** NAP-COUNT **Domain:** `count` **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.count`; domain presence signals availability. ### Description NAP-COUNT lets napplets request counts for [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) filters without downloading matching events. It is for counts such as reactions, replies, reposts, quotes, reports, and follower totals where event payloads are unnecessary. The napplet supplies filters. The runtime owns relay choice, COUNT support, aggregation, caching, approximation policy, and refusal handling. The `count` domain has no required NAP dependency. Using a relay as an internal source does not require exposing the `relay` domain to the napplet. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `query` | `filters`, `options?` | `CountResult` | `count.query` / `count.query.result` | #### Schemas `CountFilter` fields follow [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md): | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ids` | no | list of text | One or more event IDs in the upstream format. | | `authors` | no | list of text | One or more public keys in the upstream format. | | `kinds` | no | list of integer | One or more event kind numbers. | | `since` | no | integer | Non-negative inclusive lower timestamp bound in seconds. | | `until` | no | integer | Non-negative inclusive upper timestamp bound in seconds. | | `limit` | no | integer | Non-negative upstream filter limit. | | `#` | no | list of text | One or more tag values; the letter is a single ASCII letter, such as `#e`, `#p`, `#q`, or `#a`. | `CountOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `approximate` | no | boolean | Permit an estimated count. Defaults to false. | | `hll` | no | boolean | Request a HyperLogLog (HLL) value when available. Defaults to false. | Omitting `options` is equivalent to setting both options to false. `CountResult` MUST be exactly one of `CountSuccess` or `CountFailure`. `CountSuccess` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ok` | yes | boolean | MUST be true. | | `count` | yes | integer | Non-negative count; zero is a successful result. | | `approximate` | no | boolean | MUST be true for an estimate. Omission means false. | | `hll` | no | text | [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md#hll-encoding) HLL value, only when requested and available. | | `relays` | no | list of text | One or more relay URLs used, when safe to disclose. Omit when no relay was used. | A successful result MUST NOT contain `error` or `reason`. `CountFailure` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ok` | yes | boolean | MUST be false. | | `error` | yes | text | Non-empty machine-readable error code; see Error Handling. | | `reason` | no | text | Human-readable explanation. | A failed result MUST NOT contain `count`, `approximate`, `hll`, or `relays`. `filters` MUST be a non-empty list of `CountFilter`, and every list-valued field in each filter MUST contain at least one value. Multiple filters are ORed and aggregated into one count, matching [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) `COUNT` semantics. ### Operation Rules | Operation | Rules | | --- | --- | | `query` | Counts events matching [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) filters. MUST NOT return event payloads. MAY use [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) `COUNT`, runtime indexes, caches, or other runtime-owned sources. | If `options.approximate` is false, the runtime MUST NOT return an estimated count. If it cannot provide an exact count, it MUST reject with `"exact-count-unavailable"`. Setting this option to true permits an estimate; it does not require one. If `options.hll` is true, the runtime MAY return an HLL value compatible with [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) when available. If false, it MUST omit `hll`. Requesting HLL data does not permit an estimated `count` unless `options.approximate` is also true. ### Common Filters These are examples, not separate methods: | Count | Filter | | --- | --- | | reactions to event | `{ "kinds": [7], "#e": [eventId] }` | | replies to event | `{ "kinds": [1], "#e": [eventId] }` | | reposts of event | `{ "kinds": [6], "#e": [eventId] }` | | quotes of event | `{ "kinds": [1, 1111], "#q": [eventId] }` | | reports of event | `{ "kinds": [1984], "#e": [eventId] }` | | npub followers | `{ "kinds": [3], "#p": [pubkey] }` | ### NIP Mapping | Concept | NIP tie | | --- | --- | | `filters` | [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) filter objects. | | multiple filters | [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) `COUNT` OR semantics with one aggregated count. | | `approximate` | [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) approximate count flag. | | `hll` | [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) HyperLogLog response value. | ### Wire Protocol `count.*` messages use [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format: `{ "type": "domain.action", ...payload }`. | Type | Direction | Payload fields | | --- | --- | --- | | `count.query` | napplet -> runtime | `id`, `filters`, `options?` | | `count.query.result` | runtime -> napplet | `id`, `ok`, `count?`, `approximate?`, `hll?`, `relays?`, `error?`, `reason?` | The runtime MUST respond with the same `id` as the request. Result payloads MUST satisfy `CountSuccess` or `CountFailure`; the optional markers in the wire table are conditional on that choice, not permission to omit required fields. #### Examples **Exact count with default options:** ```json { "type": "count.query", "id": "c1", "filters": [{ "kinds": [1] }] } ``` ```json { "type": "count.query.result", "id": "c1", "ok": true, "count": 0 } ``` **Approximation explicitly permitted:** ```json { "type": "count.query", "id": "c2", "filters": [{ "kinds": [1] }], "options": { "approximate": true } } ``` ```json { "type": "count.query.result", "id": "c2", "ok": true, "count": 1200, "approximate": true } ``` **Exact count unavailable for the default request:** ```json { "type": "count.query.result", "id": "c1", "ok": false, "error": "exact-count-unavailable", "reason": "Only an estimate is available." } ``` `{ "ok": true }` and `{ "ok": false, "count": 10 }` are invalid results. ### Error Handling Common errors: `"invalid-filter"`, `"unsupported-filter"`, `"count-unavailable"`, `"exact-count-unavailable"`, `"relay-refused"`, `"too-expensive"`, `"policy-denied"`, `"timeout"`, `"unsupported"`. The runtime MUST reject filters it cannot safely count. Rejection is preferable to fetching large event sets or returning misleading counts. ### Runtime Behavior - MUST accept [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) filters and preserve [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) OR semantics for multiple filters. - MUST NOT return matching event payloads through NAP-COUNT. - MUST disclose approximation with `approximate: true`. - MUST keep relay selection and aggregation policy runtime-owned. - SHOULD return `relays` when useful and safe to disclose. - MAY cache, precompute, merge relay `COUNT` responses, merge HLL values, or use local indexes. - MAY refuse expensive, private, unsupported, or policy-sensitive filters. ### Security Considerations - Counts can reveal user activity and moderation signals. Runtimes MAY restrict sensitive filters. - Counts can be approximate, relay-biased, spam-inflated, or policy-filtered. Napplets MUST treat them as display hints, not proof. - HLL values are mergeable estimates, not authoritative event sets. ### References - [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) — Basic protocol flow and filters - [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) — Event Counts ### Implementations - None yet. ### Changelog - `9d097a0` - Introduced NAP-COUNT for runtime-owned event-count queries over Nostr filters. - `8995853` - Renamed the duplicated count operation to `query` / `count.query`. - `5f7a0df` - Adopted injected-domain availability and linked the current upstream web binding. - `c9e0e4c` - Defined false defaults for omitted approximation and HLL options. - `b963eb2` - Required non-empty list-valued filter attributes. - `c402531` - Specified the full count.query.result response type. - `bda6d77` - Removed the required relay dependency, defined exclusive success and failure results, and enforced approximation and HLL option semantics with correlated response examples. [Join the discussion · PR #69 ↗](https://github.com/napplet/naps/pull/69)[Read proposed changes ↗](https://github.com/napplet/naps/pull/69/files) On this pageSpecification metadata Capability domain `count` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `9353a0c6c02c` [View source on GitHub ↗](https://github.com/napplet/naps/pull/69/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-CVM / PR #31 # ContextVM Bridge Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** Session and registry bindings are tested; upstream explicitly marks the contract NEEDS WORK. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/31), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/28cf1bdb5c6412bfbfcecb79a5a5121e18376f2f/naps/NAP-CVM.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/31/files). ## NAP-CVM ### ContextVM Bridge `draft` **NAP ID:** NAP-CVM **Domain:** `cvm` **Depends:** - `value` — layering · optional — the shell MAY reuse its `value` payment backend for supported ContextVM payment methods; no `value` wire types cross the `cvm` seam. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.cvm`; domain presence signals availability. ### Description NAP-CVM provides napplets with native access to ContextVM servers through the shell. ContextVM transports Model Context Protocol (MCP) messages over Nostr relays using public-key addressing and encrypted relay messages. A napplet could build this itself on top of NAP-RELAY, but doing so would force every napplet to implement ContextVM discovery, initialization, JSON-RPC correlation, relay routing, encryption, signing, and optional payment handling. NAP-CVM moves that legwork into the runtime. The napplet supplies the server identity and the MCP operation it wants. The shell handles ContextVM transport details, user policy, signing, encryption, relay access, and value-transfer prompts. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `discover` | optional `query` (`CvmDiscoverQuery`) | list of `CvmServer` | `cvm.discover` / `cvm.discover.result` | | `request` | `server` (`CvmServerRef`), `message` (`McpMessage`), optional `options` (`CvmRequestOptions`) | `McpMessage` or `null` | `cvm.request` / `cvm.request.result` | | `listTools` | `server` (`CvmServerRef`), optional `options` (`CvmRequestOptions`) | list of `McpTool` | wrapper over `request` | | `callTool` | `server` (`CvmServerRef`), `name` (text), optional `args` (`JsonObject`), optional `options` (`CvmRequestOptions`) | `McpToolResult` | wrapper over `request` | | `listResources` | `server` (`CvmServerRef`), optional `options` (`CvmRequestOptions`) | list of `McpResource` | wrapper over `request` | | `readResource` | `server` (`CvmServerRef`), `uri` (text), optional `options` (`CvmRequestOptions`) | list of `McpResourceContent` | wrapper over `request` | | `close` | `server` (`CvmServerRef`) | none | `cvm.close` / `cvm.close.result` | | `onEvent` | handler for `CvmEvent` | `Subscription` handle | `cvm.event` | | `registry.list` | optional `query` (`CvmRegistryQuery`) | list of `CvmRegistryEntry` | `cvm.registry.list` / result | | `registry.has` | `selector` (`CvmRegistrySelector`), optional `options` (`CvmRegistryOptions`) | boolean | `cvm.registry.has` / result | | `registry.describe` | `selector` (`CvmRegistrySelector`), optional `options` (`CvmRegistryOptions`) | `CvmRegistryEntry` | `cvm.registry.describe` / result | | `registry.call` | `selector` (`CvmRegistrySelector`), `tool` (text), optional `args` (`JsonObject`), optional `options` (`CvmRegistryCallOptions`) | `McpToolResult` | `cvm.registry.call` / result | #### Schemas Primitive references: | Name | Meaning | | --- | --- | | `JsonObject` | JSON object. | | `JsonSchema` | JSON Schema object. | | `McpTool` | MCP tool object. | | `McpToolResult` | MCP tool result object. | | `McpResource` | MCP resource object. | | `McpResourceContent` | MCP resource content object. | | `Subscription` | Handle whose `close()` operation removes the local event handler; it does not close a server session. | Enumerations: | Name | Values | | --- | --- | | `CvmRequestOptions.payment` | `deny`, `prompt`, `allow` | | `CvmRegistryCallOptions.payment` | `deny`, `prompt`, `allow` | | `CvmRegistryCallOptions.cache` | `default`, `reload`, `no-store` | | `McpMessage.jsonrpc` | `"2.0"` | `CvmServerRef`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `pubkey` | text | yes | ContextVM server pubkey as 64 lowercase hexadecimal characters. | | `relays` | list of text | no | Relay hints for the server. | `CvmDiscoverQuery`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `search` | text | no | Search string. | | `kinds` | list of integer | no | Nonnegative announcement kinds to inspect; omitted means all supported announcement kinds. | | `relays` | list of text | no | Relay hints for discovery. | | `limit` | integer | no | Positive maximum number of servers to return. | `CvmServer`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `pubkey` | text | yes | ContextVM server pubkey. | | `relays` | list of text | no | Relay hints for the server. | | `name` | text | no | Human-readable server name. | | `description` | text | no | Human-readable server description. | | `capabilities` | list of text | no | MCP server capability names, such as `tools`, `resources`, or `prompts`; never tool names or common-schema hashes. Omit when unknown. | | `paymentRequired` | boolean | no | Advisory shell-derived pricing hint; see Payment handling. Omit when unknown. | `CvmRequestOptions`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `timeoutMs` | integer | no | Positive timeout in milliseconds, at most 300000; default 30000. See Timeouts. | | `initialize` | boolean | no | Whether the shell should initialize the MCP session if needed. | | `payment` | `deny`, `prompt`, or `allow` | no | Requested payment behavior; default `prompt`. Never grants spending authority. See Payment handling. | `CvmRegistryQuery`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `search` | text | no | Search string. | | `family` | text | no | Shell-local family label. | | `schemaHash` | text | no | Required CEP-15 tool schema hash; portable matching key. | | `category` | text | no | Best-effort CEP-15 `t` category hint. | | `limit` | integer | no | Positive maximum number of entries to return. | `CvmRegistrySelector`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemaHash` | text | no | CEP-15 tool identity; authoritative when present. | | `family` | text | no | Shell-local label used only when `schemaHash` is absent. | At least one selector field MUST be present. A supplied `schemaHash` MUST be a 64-character lowercase hexadecimal SHA-256 digest. A family label MUST be nonempty. Hash and label value constraints also apply when those fields are supplied in registry queries. `CvmRegistryOptions`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `server` | `CvmServerRef` | no | Required provider server. | `CvmRegistryCallOptions`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `server` | `CvmServerRef` | no | Required provider server. | | `timeoutMs` | integer | no | Positive timeout in milliseconds, at most 300000; default 30000. See Timeouts. | | `initialize` | boolean | no | Whether the shell should initialize the MCP session if needed. | | `payment` | `deny`, `prompt`, or `allow` | no | Requested payment behavior; default `prompt`. Never grants spending authority. See Payment handling. | | `cache` | `default`, `reload`, or `no-store` | no | Registry cache policy. | `CvmRegistryEntry`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `family` | text | no | Shell-local display label; not portable identity. | | `description` | text | no | Human-readable family description. | | `schemaHashes` | list of text | yes | Distinct CEP-15 hashes of all entries in `tools`; order has no meaning. | | `selected` | `CvmServerRef` | no | Selected provider implementing the complete tool set, if verified and reachable. | | `providers` | list of `CvmServerRef` | no | Candidate servers advertising the complete tool set; not proof of liveness. | | `tools` | list of `CvmRegistryTool` | yes | Nonempty tool set described by this entry. | `CvmRegistryTool`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | text | yes | Tool name. | | `description` | text | no | Human-readable tool description. | | `inputSchema` | `JsonSchema` | yes | Tool input schema. | | `outputSchema` | `JsonSchema` | no | Tool output schema. | | `schemaHash` | text | yes | Verified CEP-15 hash for this tool, including its name. | `McpMessage`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `jsonrpc` | `"2.0"` | yes | JSON-RPC version. | | `id` | text or integer | no | JSON-RPC correlation ID; never `null`. See Message handling. | | `method` | text | no | JSON-RPC method name. | | `params` | `JsonObject` | no | MCP parameters. | | `result` | `JsonObject` | no | MCP result. | | `error` | `JsonObject` | no | MCP error with integer `code`, text `message`, and optional `data`. | `CvmEvent`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `server` | `CvmServerRef` | yes | Server that emitted the event. | | `message` | `McpMessage` | yes | MCP message delivered by the server. | #### Discovery `discover(query?)` returns public ContextVM server announcements known to the shell. Sources MAY include configured relays, cached announcements, and curated lists. Discovery is not proof of reachability or permission to call a server. The shell MUST honor supplied `kinds` and `limit` filters, including for cached results. An empty `kinds` list returns no servers; an unsupported kind returns `"unsupported kind"`, rather than silently broadening the search. `search` is a best-effort text filter. Relay hints remain subject to shell policy. `CvmServer.capabilities` names MCP capabilities learned from initialization or [CEP-6 announcements](https://docs.contextvm.org/reference/ceps/cep-6/), including capability-list content. A tools announcement can establish `tools` even without CEP-15 tags. The shell MUST NOT substitute `i`\-tag hashes or tool names for MCP capability names. It SHOULD resolve verified [CEP-17 relay lists](https://docs.contextvm.org/reference/ceps/cep-17/) when available, respecting their direction markers and local relay policy. Discovery relay locations alone do not establish a server's operational relay set. #### Message handling `request(server, message, options?)` sends one MCP JSON-RPC message. The shell MUST validate it before publishing. Missing messages, batches, invalid IDs, and invalid field combinations return `"invalid message"`. The forms follow [MCP](https://modelcontextprotocol.io/specification/2025-11-25/basic): | MCP form | Fields | Completion of the enclosing `cvm.request` | | --- | --- | --- | | Request | `method` and `id`; no `result` or `error` | Matching MCP response in `message`. | | Notification | `method`, no `id`, `result`, or `error` | After transport publication, result without `message`; API returns `null`. No MCP response is awaited. | | Response | `id`, exactly one of `result` or `error`; no `method` or `params` | After transport publication, result without `message`; API returns `null`. No further MCP response is awaited. | The shell MUST preserve the supplied MCP message's form and payload. It MUST NOT add an ID to a notification or rewrite the ID of a napplet's response to a server request. For outgoing requests only, it MAY map IDs to unique transport IDs if it restores the caller's ID in the returned MCP response and consistently maps request references such as cancellation. Publication completion does not prove remote processing. The envelope ID correlates the napplet-to-shell exchange only. Correlations MUST include the authenticated napplet endpoint, server, session, and direction. Identical IDs from different napplets MUST NOT collide. An ID already used for a client request in the same MCP session returns `"invalid message"`; a response must match an outstanding server request delivered to that napplet. An unmatched response returns `"invalid message"`. `notifications/cancelled` may refer only to this napplet's requests. Shell-owned initialization, payment messages, and internal retries MUST use isolated correlation state; raw messages MUST NOT bypass shell policy. `listTools`, `callTool`, `listResources`, and `readResource` wrap the corresponding MCP operations using the same request, timeout, and payment rules. `readResource` MUST return the complete ordered `contents` list from [MCP resources/read](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), including every text or blob entry. It MUST NOT select only the first entry. #### Sessions and events Session state is scoped to an authenticated napplet endpoint and server. An accepted outbound MCP request or notification establishes that endpoint's participation; `discover` and `onEvent` alone do not open a session. The shell MAY initialize automatically and SHOULD initialize when requested or required by the server. Shared transport resources MUST preserve this isolation. `close(server)` releases only the calling napplet endpoint's subscriptions, initialization state, pending correlations, and session authorizations. It MUST complete its pending operations with `"session closed"` before acknowledging the close. It MUST NOT interrupt another napplet's requests or subscriptions. Shared transport teardown is permitted only when no other endpoint needs it. Closing an already closed session succeeds. Endpoint destruction performs the same cleanup; local cleanup does not promise remote cancellation or a payment refund. `onEvent(handler)` registers for MCP notifications and server-initiated requests from this endpoint's open sessions. The shell MUST deliver each `cvm.event` only to its owning endpoint; the binding invokes that endpoint's registered handlers. It MUST NOT broadcast session messages to unrelated napplets. Notifications correlated to a request, such as progress, use that request's owner. Payment control messages are handled by the shell under Payment handling. A napplet SHOULD register its handler before opening a session. Registration does not create transport state. Without a handler, the binding MAY discard notifications; it MUST NOT redirect messages to another napplet. Server requests still require a response or expire under session policy. Removing a handler via `Subscription.close()` does not close the server session. #### Client identity The shell selects and controls the ContextVM signing key. Fresh ephemeral keys per napplet endpoint and shell session are RECOMMENDED. The ContextVM client pubkey is not the user's Nostr identity and MUST NOT be presented as such. [CEP-16 clientPubkey](https://docs.contextvm.org/reference/ceps/informational/cep-16/) identifies this transport signer, not the human user. The shell MUST isolate session negotiation, inbound server requests, paid execution authorizations, and allowance accounting between napplets. If remote state is keyed only by client pubkey, the shell MUST use distinct client keys where sharing would let one napplet consume another's authorization or receive its messages. Sharing a relay connection does not authorize sharing that state. Closing a session MUST prevent its remaining authorization from being reused by another napplet, even if the server retains state after local closure. #### Timeouts `timeoutMs` defaults to 30000 and MUST be an integer from 1 through 300000. Out-of-range or malformed values return `"invalid timeout"` before publication. The shell MAY impose a lower policy maximum, but MUST reject a requested duration above that maximum with `"timeout out of policy"`; it MUST NOT silently clamp it. The shell MUST enforce the accepted duration from receipt through queueing, initialization, consent, payment, and final response or publication. Retries and progress events MUST NOT reset the deadline. Expiry returns `"relay timeout"`, releases the pending correlation, and prevents further automatic spending or retries for that operation. A late response MUST NOT produce a second result or be redelivered as `cvm.event`. Timeout does not prove remote cancellation or undo an already initiated transfer. Projection bindings MUST honor the requested duration and permit delivery of the shell's final result after its deadline, accounting for projection delivery latency. In particular, a fixed 30-second binding timer MUST NOT cut off an accepted 120-second request. This applies equally to convenience wrappers and `registry.call`; other request/result operations use the default duration. #### Payment handling Payment processing is optional; payment policy enforcement is mandatory. [CEP-8](https://docs.contextvm.org/reference/ceps/cep-8/) owns pricing, payment methods, negotiation, correlation, and settlement messages. The shell derives `paymentRequired` from verified pricing information: `true` when a positive `cap` price is advertised, `false` only when known advertised capabilities are explicitly free, and omitted when pricing is unknown. It is an advisory hint, not a quote or guarantee about any invocation. `pmi` advertises methods, not a price; missing pricing or a `pmi` tag alone MUST NOT imply free service. `payment` defaults to `prompt`. It constrains the shell as follows: | Option | Settlement authority | Requested CEP-8 session mode | | --- | --- | --- | | `deny` | No new payment or consumption of paid authorization for this invocation. Free calls remain usable. | `explicit_gating` preferred. | | `prompt` | Explicit user consent for this invocation, with server, operation, amount, and payment method shown by the shell. | `explicit_gating` preferred. | | `allow` | Only a standing user allowance configured outside the napplet, covering this napplet, server, operation, method, amount, and remaining budget. No allowance means `"payment denied"` when payment is needed. | `transparent` permitted; `explicit_gating` also permitted. | The shell MUST negotiate and track the effective `payment_interaction` at the session level, following CEP-8 and [CEP-35](https://docs.contextvm.org/reference/ceps/informational/cep-35/). A request option is not a per-message transport mode. Mode changes MUST follow upstream negotiation and MUST NOT change the lifecycle of another in-flight operation. The shell MAY retain the current mode while enforcing each invocation's policy. If explicit gating is rejected, transparent fallback is permitted only when the shell can enforce the same consent and budget constraints. Otherwise the call fails with `"payment required"`; fallback MUST NOT grant spending authority. The shell MUST intercept and correlate `-32042` payment gates and `notifications/payment_required` before delivering application results. It MUST verify the server and the owning invocation, validate the offered amount and method, and apply the table above. These transport messages MUST NOT become a napplet-controlled payment path. A shell without a compatible payment handler returns `"payment required"` promptly; a policy prohibition or user refusal returns `"payment denied"`. Neither outcome may hang awaiting a payment. The same policy applies to already-paid execution authorizations: the shell MUST check ownership and consent before sending an invocation that could consume one, not just before making a new transfer. In transparent mode the shell settles an authorized offer and awaits the original response. In explicit gating it settles one authorized offer, then retries the same method and params under the same client identity as prescribed by CEP-8, using a fresh internal request ID and restoring the caller's ID in the final response. It MUST track payment-pending responses and paid authorizations without duplicate settlement. It MUST NOT blindly retry after an uncertain execution: a paid authorization does not guarantee replay of a lost result. Every retry remains within the original timeout and consent or allowance. Concurrent calls MUST reserve allowance budget atomically. A napplet MUST NOT borrow another napplet's paid authorization, including through an otherwise identical call. The shell MAY reuse a compatible `value` backend internally. Exposing `value` to the napplet is not required, and its presence does not guarantee support for a server's payment method. Unsupported payment methods fail as above; this NAP does not define new value rails. Signing keys and settlement credentials remain outside the napplet. #### Registry identity and matching A registry family is a set of CEP-15 tool schema hashes. Its `family` name is a shell-local label, not portable identity. `CvmRegistryEntry.schemaHashes` MUST equal the distinct hashes of its `tools`. There is no aggregate family `schemaHash`. Hashes are computed and verified by the shell according to [CEP-15](https://docs.contextvm.org/reference/ceps/cep-15/); NAP-CVM defines no alternative hashing or schema-version algorithm. `registry.list(query?)` returns known entries. `category` uses CEP-15 `t` tags as best-effort discovery hints. A `schemaHash` query finds the matching tool across local family names and returns singleton tool entries. Without a hash, `family` filters by local label. Search and category filters further narrow candidates; the positive `limit`, when present, bounds the returned count. `registry.has(selector, options?)` returns whether a permitted, verified, recently reachable provider satisfies the selector and optional server pin. It returns `false` for no match or an unavailable registry implementation. `registry.describe` returns the matching entry and schemas; no match is `"schema mismatch"` for a hash selector or `"family not found"` for a label. A selector's `schemaHash` is authoritative even if its optional `family` label is unknown or different on this shell. Hash selection returns a singleton tool entry and MUST NOT require registration under any local family name. Without a hash, the shell selects the complete tool set for the named family. `selected` and `providers` refer to providers of that complete returned set. Shells MUST verify tool hashes before exposing them as matching schemas; announcements are candidates, not proof of schema compatibility or liveness. `registry.call(selector, tool, args?, options?)` invokes MCP `tools/call` under the same policy as `callTool`. The requested tool name MUST match the selected tool's verified schema hash; mismatch returns `"schema mismatch"` before publication. A supplied server pin MUST be honored; no other provider may substitute. The shell MAY select a different provider only before dispatch and only with the same verified tool hash and no server pin. An uncertain outcome after dispatch MUST NOT trigger automatic replay on another provider. The shell MAY cache discovery, schemas, and results under user policy. Result cache keys MUST include the tool schema hash, arguments, selected server, and napplet policy scope. Cache hits MUST pass the current policy checks. The cache option is `default` (local policy), `reload` (bypass stored results, may store the fresh result), or `no-store` (neither read nor write result cache). Discovery and schema caches are independent. A prohibited cache choice returns `"cache policy denied"`. ### Wire Protocol `cvm.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `cvm.discover` | napplet -> shell | `id`, `query?` | | `cvm.discover.result` | shell -> napplet | `id`, `servers?`, `error?` | | `cvm.request` | napplet -> shell | `id`, `server`, `message`, `options?` | | `cvm.request.result` | shell -> napplet | `id`, `message?`, `error?` | | `cvm.close` | napplet -> shell | `id`, `server` | | `cvm.close.result` | shell -> napplet | `id`, `error?` | | `cvm.event` | shell -> napplet | `server`, `message` | | `cvm.registry.list` | napplet -> shell | `id`, `query?` | | `cvm.registry.list.result` | shell -> napplet | `id`, `entries?`, `error?` | | `cvm.registry.has` | napplet -> shell | `id`, `selector`, `options?` | | `cvm.registry.has.result` | shell -> napplet | `id`, `has?`, `error?` | | `cvm.registry.describe` | napplet -> shell | `id`, `selector`, `options?` | | `cvm.registry.describe.result` | shell -> napplet | `id`, `entry?`, `error?` | | `cvm.registry.call` | napplet -> shell | `id`, `selector`, `tool`, `args?`, `options?` | | `cvm.registry.call.result` | shell -> napplet | `id`, `result?`, `error?` | Key design notes: - `cvm.request.result` correlates to the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) request `id`; the embedded MCP message retains its own JSON-RPC `id`. - `cvm.event` carries no envelope `id`. Delivery is scoped to the owning napplet endpoint and open session as defined above. - The shell owns all ContextVM event IDs, relay subscriptions, `p` tags, `e` tags, encryption state, and signing. - The shell MAY initialize a ContextVM session automatically before the first capability request. - Registry messages are shell-level conveniences. They do not define new ContextVM server behavior. - Registry selectors pin a tool with `selector.schemaHash`; returned entries enumerate the tool set in `schemaHashes`. Family labels do not override hash matching. #### Examples Hashes and pubkeys in these examples are abbreviated for readability; actual requests use complete values. **Discover servers:** ``` -> { "type": "cvm.discover", "id": "d1", "query": { "search": "relay", "limit": 5 } } <- { "type": "cvm.discover.result", "id": "d1", "servers": [ { "pubkey": "65a334...", "relays": ["wss://relay.example.com"], "name": "RelayVM", "description": "Relay intelligence exposed through MCP over Nostr" } ] } ``` **Call an MCP tool through ContextVM:** ``` -> { "type": "cvm.request", "id": "r1", "server": { "pubkey": "65a334...", "relays": ["wss://relay.example.com"] }, "message": { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_relay", "arguments": { "url": "wss://relay.example.com" }, "_meta": { "progressToken": "tool-2" } } }, "options": { "initialize": true, "payment": "prompt" } } <- { "type": "cvm.request.result", "id": "r1", "message": { "jsonrpc": "2.0", "id": 2, "result": { "content": [{ "type": "text", "text": "{...}" }], "isError": false } } } ``` **Server notification:** ``` <- { "type": "cvm.event", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "method": "notifications/progress", "params": { "progressToken": "tool-2", "progress": 50 } } } ``` **Browse a local family, then call by portable tool hash:** ``` -> { "type": "cvm.registry.has", "id": "h1", "selector": { "family": "relatr" } } <- { "type": "cvm.registry.has.result", "id": "h1", "has": true } -> { "type": "cvm.registry.describe", "id": "s1", "selector": { "family": "relatr" } } <- { "type": "cvm.registry.describe.result", "id": "s1", "entry": { "family": "relatr", "schemaHashes": ["a7f3d9..."], "selected": { "pubkey": "65a334...", "relays": ["wss://relay.example.com"] }, "tools": [ { "name": "search_profiles", "schemaHash": "a7f3d9...", "inputSchema": { "type": "object", "properties": { "q": { "type": "string" } }, "required": ["q"] } } ] } } -> { "type": "cvm.registry.call", "id": "c1", "selector": { "schemaHash": "a7f3d9..." }, "tool": "search_profiles", "args": { "q": "jack" } } <- { "type": "cvm.registry.call.result", "id": "c1", "result": { "content": [{ "type": "text", "text": "{...}" }], "isError": false } } ``` **Publish a notification without creating an MCP request:** Here MCP request `2` is still pending for this napplet. ``` -> { "type": "cvm.request", "id": "n1", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "method": "notifications/cancelled", "params": { "requestId": 2 } } } <- { "type": "cvm.request.result", "id": "n1" } ``` **Answer a server request, preserving its ID:** ``` <- { "type": "cvm.event", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "id": "srv-7", "method": "ping" } } -> { "type": "cvm.request", "id": "answer-1", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "id": "srv-7", "result": {} } } <- { "type": "cvm.request.result", "id": "answer-1" } ``` **Unsupported registry and denied request:** ``` -> { "type": "cvm.registry.list", "id": "l1" } <- { "type": "cvm.registry.list.result", "id": "l1", "error": "unsupported method" } -> { "type": "cvm.registry.has", "id": "h2", "selector": { "family": "relatr" } } <- { "type": "cvm.registry.has.result", "id": "h2", "has": false } -> { "type": "cvm.request", "id": "denied-1", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "id": 3, "method": "tools/list" } } <- { "type": "cvm.request.result", "id": "denied-1", "error": "policy denied" } ``` #### Error Handling A shell exposing `cvm` MUST recognize every napplet-to-shell request type in the wire table and return exactly one corresponding `.result` with the same envelope `id`, including failures and notification/response publication acknowledgments. Recognized but unimplemented operations return `"unsupported method"`, except `registry.has`, which returns `has: false` when registry support is absent. Truly unknown message types retain the silent-ignore behavior defined by [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303). Shell and transport failures MUST use the top-level text `error` in that result. There are no `cvm.*.error` messages; ACL denial returns the matching `.result` with `error: "policy denied"`. On a shell error, success payload fields MUST be omitted. On success, the operation's payload is required, except for `close` and notification/response publication, whose result has only `type` and `id`. | Error | Meaning | | --- | --- | | `invalid message` | Missing or malformed MCP message, invalid field combination, duplicate client request ID, or unmatched server response. | | `invalid request` | Malformed server reference, query, selector, or other non-message parameter. | | `invalid timeout` | Timeout is not an integer in the permitted range. | | `timeout out of policy` | Valid timeout exceeds the shell's configured maximum. | | `unsupported method` | Recognized NAP operation is not implemented. | | `unsupported kind` | Requested announcement kind is not supported. | | `server not found` | Server cannot be resolved. | | `relay timeout` | Operation deadline expired. | | `initialization failed` | Required MCP initialization failed. | | `session closed` | The calling endpoint released the session while work was pending. | | `payment required` | A payment gate cannot be fulfilled with available payment support or negotiation. | | `payment denied` | Request policy, allowance, or user decision disallows payment. | | `policy denied` | Shell ACL disallows the operation. | | `family not found` | Local family label has no match. | | `schema mismatch` | Pinned tool hash or tool name cannot be satisfied. | | `provider unavailable` | Matching providers are not reachable. | | `cache policy denied` | Requested cache behavior is forbidden. | Remote MCP errors MUST remain in `message.error` for raw `request`, except for payment control handled above. Convenience wrappers MUST surface remote errors as failures, not successful empty results. A registry tool failure uses its `McpToolResult` when available; a remote JSON-RPC error becomes a top-level text error describing that failure. Validation errors MUST NOT be reported as `"unsupported method"`. ### Shell Behavior - The shell MUST sign, encrypt, route, and verify ContextVM events outside the napplet sandbox. Relay delivery alone is not proof of the server's identity. - The shell MUST enforce the session, message, timeout, payment, registry, and result rules above, including when a feature is unsupported. - The shell MUST NOT expose direct relay sockets, transport tags, or signing authority to a napplet through this domain. - Initialization and cached state MUST preserve per-napplet isolation. Advertised MCP client capabilities MUST reflect features the shell and target napplet can actually service; forwarding a server request does not itself implement one. - Transport implementation is runtime-owned. The [ContextVM SDK](https://docs.contextvm.org/reference/ts-sdk/transports/nostr-client-transport/) or another conforming transport MAY be used; either must preserve these NAP contracts and the linked upstream protocols. ### Conformance cases These cases exercise the contract; they are not implementation test results. | Case | Required outcome | | --- | --- | | A and B call the same server using MCP ID `1`; A closes. | A's pending call fails with `session closed`; B's call and events remain usable. | | A registers `onEvent` without an open session; B receives a server request. | A receives nothing. Only B can answer the request. | | A sends a notification, then answers a server request. | Neither awaits an MCP response; notification stays ID-less, response ID is preserved, each envelope receives one acknowledgment. | | Missing message, `id: null`, or both `result` and `error`. | `invalid message` in `cvm.request.result`, with no publication. | | Registry is not implemented; an ACL denies a direct request. | Registry operations return `unsupported method` (`has` returns false); ACL denial uses the matching `.result`, never `.error`. | | A 120000 ms call returns after 45000 ms. | It succeeds if permitted by policy; no fixed 30000 ms binding timeout. | | Timeout is zero, above 300000, or above a lower shell maximum. | `invalid timeout` for the first two; `timeout out of policy` for the last, before publication. | | A call expires or closes before its response arrives. | Exactly one result; the late response is not an event or a second result. | | `deny`, refused `prompt`, or `allow` without sufficient allowance meets a payment gate. | `payment denied`; no new settlement or unauthorized consumption of prior payment. | | A shell without payment support receives `-32042` or `notifications/payment_required`. | `payment required` promptly, without a stalled MCP call. | | Two napplets make the same paid call concurrently. | No shared authorization consumption or budget overrun; session modes remain isolated. | | Two shells use different family labels for the same tool hash. | The same hash selector resolves on both; returned `schemaHashes` equals the set of tool hashes. | | A tool hash/name mismatches, or a pinned provider is unavailable. | No substitute tool or provider is called; `schema mismatch` or `provider unavailable`. | | `resources/read` returns several text/blob entries. | `readResource` returns all entries in order. | | A tools announcement has no CEP-15 `i` tags; query restricts announcement kinds. | Capability names still derive from MCP data; only the requested supported kinds contribute results. | ### Security Considerations - ContextVM servers are remote programs. Shells SHOULD treat tool calls as untrusted remote execution and enforce per-napplet policy for which servers and methods may be called. - MCP tool arguments can contain sensitive user data. Shells SHOULD expose consent or ACL controls before allowing a napplet to send data to a new server. - Shells MUST verify server identity by public key. A relay delivering a response is not proof that the response came from the requested server. - Shells MUST enforce the timeouts above and SHOULD apply per-napplet concurrency limits to prevent resource exhaustion. - Payment requests from ContextVM servers MUST NOT be auto-approved unless covered by an explicit user allowance. - Napplets receive MCP results but not ContextVM private keys, encryption keys, NWC secrets, relay credentials, or direct socket access. - Registry discovery is not trust. Shells SHOULD apply the same per-napplet policy to `registry.call` that they apply to direct `callTool`. - Shells SHOULD NOT serve cached registry tool results across napplets when the arguments or selected provider are user-sensitive or policy-scoped. ### References - [ContextVM](https://contextvm.org/) - [ContextVM protocol specification](https://docs.contextvm.org/reference/spec/ctxvm-draft-spec/) - [ContextVM CEP-8 Capability Pricing and Payment Flow](https://docs.contextvm.org/reference/ceps/cep-8/) - [ContextVM CEP-15 Common Tool Schemas](https://docs.contextvm.org/reference/ceps/cep-15/) - [Model Context Protocol](https://modelcontextprotocol.io/specification/2025-11-25/basic) ### Implementations - [kehto/web](https://github.com/kehto/web) — existing partial implementation of discovery, raw MCP requests, convenience wrappers, sessions, and events. The [2026-10-14 review](https://github.com/napplet/naps/pull/31#issuecomment-6168948959) identifies required runtime corrections and reports registry and payment handling as unimplemented. This is not a claim of conformance to this revision. ### Changelog - `e001eaf` - Introduced NAP-CVM as a native ContextVM bridge. - `1873f7e` - Added a runtime registry facade for discovering CVM surfaces. - `69d6bbd` - Adopted injected-domain availability and linked the current upstream web binding. - `12ad986` - Scoped sessions, client identities, events, and payment authorization per napplet; defined MCP message completion, bounded timeouts, result errors, portable tool-hash registry selection, complete resource reads, discovery semantics, and conformance cases. [Join the discussion · PR #31 ↗](https://github.com/napplet/naps/pull/31)[Read proposed changes ↗](https://github.com/napplet/naps/pull/31/files) On this pageSpecification metadata Capability domain `cvm` Repository state Open PR Registry status Draft Document marker draft Declared dependencies - [value](https://napplet.run/protocol/nap-value-pr-30/) Source revision `28cf1bdb5c64` [View source on GitHub ↗](https://github.com/napplet/naps/pull/31/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-DISPLAY / PR #97 # Runtime-Controlled Pixel Displays Draft PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/2c2e1b5ca3849dbab8c3a721704e30646a641079/naps/NAP-DISPLAY.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/97/files). ## NAP-DISPLAY ### Runtime-Controlled Pixel Displays `draft` **NAP ID:** NAP-DISPLAY **Domain:** `display` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.display`; domain presence signals availability. ### Description NAP-DISPLAY lets a napplet list displays that the runtime controls and push sparse batches of logical RGB pixels. The napplet supplies coordinates and color bytes. The runtime reports each display's logical geometry and device type, then maps each request to the device's color depth, orientation, layout, and refresh model. The shell mediates access and competing writes. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `list` | none | list of `DisplayDevice` | `display.list` / `display.list.result` | | `push` | `displayId` (text), `pixels` (list of `Pixel`) | `null` | `display.push` / `display.push.result` | #### Schemas `DisplayType` accepts `"lcd"`, `"eink"`, `"led-matrix"`, or `"other"`. `DisplayDevice` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Opaque runtime-assigned stable identifier. | | `width` | yes | integer | Logical width in pixels. Positive. | | `height` | yes | integer | Logical height in pixels. Positive. | | `type` | yes | `DisplayType` | Runtime-determined device type. | The runtime MUST return the same `id` for the same display across list calls, ordering changes, and runtime restarts. It MUST NOT reuse an `id` for a different display. `Pixel` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `x` | yes | integer | Zero-based logical x-coordinate. | | `y` | yes | integer | Zero-based logical y-coordinate. | | `bytes` | yes | list of integer | Exactly three sRGB bytes: red, green, blue. | Each byte MUST fall within `0..255`. Each coordinate MUST satisfy `0 <= x < width` and `0 <= y < height` for the selected display. A batch MUST contain each coordinate at most once. **`list()`** returns the displays that the shell permits the napplet to use. Each call returns a snapshot and MAY reflect connection or policy changes. **`push(displayId, pixels)`** submits one non-empty pixel batch. A successful result means the runtime accepted the entire batch. It does not promise that the hardware has completed its refresh. ### Wire Protocol `display.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `display.list` | napplet -> shell | `id` | | `display.list.result` | shell -> napplet | `id`, `displays`, `error?` | | `display.push` | napplet -> shell | `id`, `displayId`, `pixels` | | `display.push.result` | shell -> napplet | `id`, `error?` | Key design notes: - `id` correlates each request and result. - Coordinates use a top-left origin. `x` increases rightward; `y` increases downward. - RGB bytes describe logical color, not a native framebuffer. The runtime never exposes device byte order, scan order, or driver commands. - The runtime validates a whole batch before applying any pixel from it. #### Examples **List displays:** ``` -> { "type": "display.list", "id": "d1" } <- { "type": "display.list.result", "id": "d1", "displays": [ { "id": "desk", "width": 800, "height": 480, "type": "eink" }, { "id": "badge", "width": 32, "height": 8, "type": "led-matrix" } ] } ``` **Push two pixels:** ``` -> { "type": "display.push", "id": "d2", "displayId": "badge", "pixels": [ { "x": 0, "y": 0, "bytes": [255, 0, 0] }, { "x": 1, "y": 0, "bytes": [0, 32, 255] } ] } <- { "type": "display.push.result", "id": "d2" } ``` #### Error Handling Result messages MAY include `error`. This NAP defines `"not-found"`, `"unavailable"`, `"denied"`, `"invalid-pixel"`, and `"too-large"`. A failed `display.list` returns an empty `displays` list. A failed `display.push` applies no pixels. ### Runtime and Shell Behavior - The runtime MUST discover devices and determine their `id`, `width`, `height`, and `type`. A napplet cannot supply or alter descriptors. - The runtime MUST map logical RGB pixels to the selected device. It SHOULD preserve position, relative luminance, and color as closely as the device allows. - The runtime MAY rotate coordinates, reorder scan lines, quantize colors, dither, adjust brightness, or coalesce refreshes to match the device. - The runtime MUST process accepted batches in arrival order for each display. - The shell MUST mediate which displays each napplet can list and write. - The shell MUST respond to every request with the matching result and `id`. - The shell MAY reject, rate-limit, or cap batches according to device and per-napplet policy. ### Security Considerations - Display access can reveal physical hardware and affect user-visible output. The shell SHOULD require explicit per-napplet permission. - Stable identifiers can aid fingerprinting. The runtime SHOULD use opaque identifiers instead of hardware serial numbers. - Large or rapid batches can exhaust CPU, bandwidth, power, or device lifetime. The shell SHOULD bound batch size and update rate. - The fixed RGB schema prevents a napplet from sending native driver commands. ### Implementations - (none yet) ### Changelog - `62f2d8b` - Introduced NAP-DISPLAY for runtime-controlled device discovery and RGB pixel updates. - `677258c` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #97 ↗](https://github.com/napplet/naps/pull/97)[Read proposed changes ↗](https://github.com/napplet/naps/pull/97/files) On this pageSpecification metadata Capability domain `display` Repository state Draft PR Registry status Draft Document marker draft Declared dependencies None declared in the preamble Source revision `2c2e1b5ca384` [View source on GitHub ↗](https://github.com/napplet/naps/pull/97/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-DM / PR #74 # Direct Messages Draft PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** Direct-message binding has shim and type coverage; transport interoperability is not established by those tests. Implementation proposal: [draft ↗](https://github.com/napplet/naps/pull/74), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/2c4e4423f40e72675822225acacea2ee5889ade2/naps/NAP-DM.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/74/files). ## NAP-DM ### Direct Messages `draft` **NAP ID:** NAP-DM **Domain:** `dm` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.dm`; domain presence signals availability. ### Description NAP-DM lets a napplet present direct-message UI while the runtime owns the message implementation. The napplet asks for conversations, message history, send, and live delivery. The runtime chooses and executes the transport, encryption, signing, relay routing, storage, and key management. This interface is generic. It does not require NIP-04, NIP-17, Double Ratchet, MLS, or any other concrete private-message protocol. A runtime MAY implement one or more of them behind the same `dm` surface. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `status` | — | `DmStatus` | `dm.status` / `dm.status.result` | | `conversations` | `DmConversationQuery` | `DmConversationPage` | `dm.conversations` / `dm.conversations.result` | | `messages` | `DmMessageQuery` | `DmMessagePage` | `dm.messages` / `dm.messages.result` | | `send` | `DmSendRequest` | `DmSendResult` | `dm.send` / `dm.send.result` | | `subscribe` | `DmSubscribeRequest` | `DmSubscription` | `dm.subscribe` / `dm.subscribe.result` | | `unsubscribe` | `subscriptionId` (`tstr`) | `DmOk` | `dm.unsubscribe` / `dm.unsubscribe.result` | #### Schemas Primitive aliases and enums: | Name | Definition | | --- | --- | | `HexPubkey` | 32-byte public key encoded as lowercase hex | | `Timestamp` | integer timestamp | | `DmConversationKind` | `direct`, `group` | | `DmMessageStatus` | `sent`, `delivered`, `received`, `failed` | `DmStatus` fields: | Field | Required | Type | | --- | --- | --- | | `available` | yes | boolean | | `ownerPubkey` | no | `HexPubkey` | | `implementations` | yes | list of text | | `capabilities` | yes | list of text | `DmConversationQuery` fields: | Field | Required | Type | | --- | --- | --- | | `cursor` | no | text | | `limit` | no | unsigned integer | `DmPeer` fields: | Field | Required | Type | | --- | --- | --- | | `pubkey` | yes | `HexPubkey` | | `label` | no | text | | `avatar` | no | text | `DmConversation` fields: | Field | Required | Type | | --- | --- | --- | | `id` | yes | text | | `kind` | yes | `DmConversationKind` | | `participants` | yes | list of `DmPeer` | | `subject` | no | text | | `unread` | yes | unsigned integer | | `updatedAt` | no | `Timestamp` | `DmConversationPage` fields: | Field | Required | Type | | --- | --- | --- | | `conversations` | yes | list of `DmConversation` | | `cursor` | no | text | `DmMessageQuery` fields: | Field | Required | Type | | --- | --- | --- | | `conversationId` | yes | text | | `cursor` | no | text | | `limit` | no | unsigned integer | `DmMessage` fields: | Field | Required | Type | | --- | --- | --- | | `id` | yes | text | | `conversationId` | yes | text | | `senderPubkey` | yes | `HexPubkey` | | `createdAt` | yes | `Timestamp` | | `content` | yes | text | | `status` | yes | `DmMessageStatus` | `DmMessagePage` fields: | Field | Required | Type | | --- | --- | --- | | `messages` | yes | list of `DmMessage` | | `cursor` | no | text | `DmSendRequest` fields: | Field | Required | Type | | --- | --- | --- | | `conversationId` | no | text | | `recipients` | yes | list of `HexPubkey` | | `content` | yes | text | | `clientMessageId` | no | text | `DmSendResult` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | | `message` | yes | `DmMessage` | `DmSubscribeRequest` fields: | Field | Required | Type | | --- | --- | --- | | `conversationId` | no | text | `DmSubscription` fields: | Field | Required | Type | | --- | --- | --- | | `subscriptionId` | yes | text | `DmOk` fields: | Field | Required | Type | | --- | --- | --- | | `ok` | yes | boolean | `DmError` fields: | Field | Required | Type | | --- | --- | --- | | `error` | yes | text | `status` returns whether the runtime can currently service direct messages for the active user. `implementations` is advisory. Values are runtime labels, not protocol negotiation. Napplets MUST NOT require a specific implementation label unless another spec defines it. `conversations` returns normalized conversation summaries. Runtime-specific thread, invite, session, and relay state stays hidden unless another NAP defines it. `messages` returns normalized cleartext messages for one conversation. The runtime MAY redact or omit messages according to policy. `send` asks the runtime to send a direct message. The runtime MAY create a new conversation when `conversationId` is omitted. The runtime MUST perform signing, encryption, routing, persistence, and policy checks outside the napplet. `subscribe` starts live delivery. When `conversationId` is omitted, the runtime MAY deliver messages for all conversations visible to the napplet. `unsubscribe` stops a live subscription. ### Wire Protocol `dm.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `dm.status` | napplet -> runtime | `id` | | `dm.status.result` | runtime -> napplet | `id`, `DmStatus` or `DmError` | | `dm.conversations` | napplet -> runtime | `id`, `DmConversationQuery` | | `dm.conversations.result` | runtime -> napplet | `id`, `DmConversationPage` or `DmError` | | `dm.messages` | napplet -> runtime | `id`, `DmMessageQuery` | | `dm.messages.result` | runtime -> napplet | `id`, `DmMessagePage` or `DmError` | | `dm.send` | napplet -> runtime | `id`, `DmSendRequest` | | `dm.send.result` | runtime -> napplet | `id`, `DmSendResult` or `DmError` | | `dm.subscribe` | napplet -> runtime | `id`, `DmSubscribeRequest` | | `dm.subscribe.result` | runtime -> napplet | `id`, `DmSubscription` or `DmError` | | `dm.unsubscribe` | napplet -> runtime | `id`, `subscriptionId` | | `dm.unsubscribe.result` | runtime -> napplet | `id`, `DmOk` or `DmError` | | `dm.message` | runtime -> napplet | `subscriptionId`, `message` (`DmMessage`) | Request/result pairs use `id` for correlation. `dm.message` is a runtime push message and has no request `id`. #### Examples **Status:** ``` -> { "type": "dm.status", "id": "s1" } <- { "type": "dm.status.result", "id": "s1", "available": true, "ownerPubkey": "", "implementations": ["nip17"], "capabilities": ["direct"] } ``` **Send:** ``` -> { "type": "dm.send", "id": "m1", "recipients": [""], "content": "hello" } <- { "type": "dm.send.result", "id": "m1", "ok": true, "message": { "id": "msg1", "conversationId": "c1", "senderPubkey": "", "createdAt": 1790337600, "content": "hello", "status": "sent" } } ``` **Subscribe:** ``` -> { "type": "dm.subscribe", "id": "sub1", "conversationId": "c1" } <- { "type": "dm.subscribe.result", "id": "sub1", "subscriptionId": "live1" } <- { "type": "dm.message", "subscriptionId": "live1", "message": { "id": "msg2", "conversationId": "c1", "senderPubkey": "", "createdAt": 1790337601, "content": "hi", "status": "received" } } ``` #### Error Handling Result messages include `error` when the runtime cannot fulfill a request. Common errors: `"unavailable"`, `"not signed in"`, `"forbidden"`, `"not found"`, `"invalid recipient"`, `"send failed"`, `"unsupported"`. When `error` is present, other result fields are undefined. ### Shell Behavior - The runtime MUST respond to every request with a result message carrying the same `id`. - The runtime MUST keep signing keys, private message keys, ratchet/session state, relay credentials, and storage adapters outside the napplet. - The runtime MUST enforce per-napplet policy before revealing conversations, message history, or live messages. - The runtime MUST validate recipient public keys before sending. - The runtime MAY choose any private-message implementation. - The runtime MAY expose implementation labels in `DmStatus.implementations`, but those labels are advisory unless another spec defines their semantics. - The runtime MAY deny history or live subscriptions while still allowing send. ### Security Considerations - Napplets receive normalized message data only. They MUST NOT receive signing keys, private message keys, raw ratchet state, relay credentials, or storage handles. - The runtime is the policy boundary for private conversation visibility. - The runtime is responsible for encryption, authentication, replay handling, sender validation, and relay routing. - The runtime SHOULD avoid exposing implementation-specific metadata that lets a napplet fingerprint peers, devices, sessions, or relay topology beyond what the user-facing DM experience requires. - Runtimes that support several DM implementations MUST avoid downgrade behavior that silently weakens user privacy without policy or user consent. ### Relation to NAP-3 and NAAT-DM NAP-3 defines `chat:open-dm`, a NAP-N topic for opening or focusing a DM handler by role. NAP-DM defines the runtime API that a DM handler can use once it is running. They are complementary. NAAT-DM names the `dm` handler role. It does not own the NAP-DM payload or API. ### Non-goals - Defining a specific private-message protocol. - Defining Double Ratchet session management, invite exchange, or device semantics. - Defining public chat rooms, stream chat, or group membership administration. - Defining profile metadata lookup. - Defining shell window placement, default handler resolution, or role dispatch. ### Implementations - Hyprgate PR #233 uses temporary `him.*` spike wire for a Double Ratchet IM prototype. That wire is not NAP-DM and should migrate to this generic `dm` interface if this spec is accepted. ### Changelog - `1d67fac` - Introduced NAP-DM as a generic runtime-mediated direct-message surface. - `4d68392` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #74 ↗](https://github.com/napplet/naps/pull/74)[Read proposed changes ↗](https://github.com/napplet/naps/pull/74/files) On this pageSpecification metadata Capability domain `dm` Repository state Draft PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `2c4e4423f40e` [View source on GitHub ↗](https://github.com/napplet/naps/pull/74/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-EMAIL / PR #139 # Shell-Mediated Email Draft PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/a63c592c78eb74ce59a6c7d315603c748cc1c61a/naps/NAP-EMAIL.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/139/files). ## NAP-EMAIL ### Shell-Mediated Email `draft` The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119.html). **NAP ID:** NAP-EMAIL **Domain:** `email` **Depends:** - `contacts` — capability · optional — `contacts.pick` selects recipient addresses; `contacts.query` and `contacts.get` support separately granted contact browsing and autocomplete. Manual recipient entry remains available without this domain. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.email`; domain presence signals availability. This is projection guidance. ### Description NAP-EMAIL lets napplets read email and submit messages using accounts configured in the shell. The napplet designs the inbox, reader, composer, and thread view. The shell mediates account selection, disclosure, and sending authority. The runtime holds credentials, retrieves mail through POP3 (Post Office Protocol version 3), and submits mail through SMTP (Simple Mail Transfer Protocol). The user can grant a reader access to incoming mail, a thread view access to retained incoming and outgoing mail, and a separate composer permission to send. #### Boundary **Account configuration is privileged runtime state.** Returning SMTP or POP3 credentials as napplet settings would give the napplet authority outside the seam. An email address, account handle, or declared domain grants no authority. | Concern | Owner | | --- | --- | | Account creation, removal, server names, ports, TLS and authentication settings | User, through trusted shell UI | | Passwords, application passwords, tokens, credential refresh, server connections | Runtime | | Account and sender selection, inbox and sent-history read grants, send grants, recipient restrictions and confirmation | Shell, under user control | | Retrieval, MIME parsing and construction, shared account message storage, private submission records | Runtime | | Inbox presentation, composing, reply and threading choices, local drafts | Napplet | | Network endpoints, protocol commands, server sessions and message-number mapping | Runtime internals | SMTP and POP3 are separate account services. An account MAY have either or both. SMTP supplies no inbox; POP3 supplies no submission service, folders, synchronized read flags, or sent-mail archive. This draft exposes a runtime-held inbox, account-level sent history, and private submission records. Sent history records submissions made through this runtime; it does not discover messages sent by other clients or create a server-side sent folder. Remote deletion, mailbox administration, raw SMTP/POP3 commands, arbitrary headers, and account-configuration APIs are outside this surface. Email reads MUST NOT cause the runtime to delete mail from the POP3 server. Any independent shell retention policy requires separate user authorization. No other NAP domain is required; contact integration is optional. Per-napplet configuration APIs are not a channel for provisioning these accounts or delivering their secrets. This NAP defines no cross-napplet convention and changes no projection semantics. MIME means Multipurpose Internet Mail Extensions: the format for email body parts, alternatives, and attachments. ### API Surface A runtime exposing `email` MUST implement every operation below. The `receive`, `readSent`, and `send` fields describe access exposed to the caller for each account; they do not override current policy. Availability is projection-defined. Manifests declare the bare domain `email`; declarations are not grants. | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `accounts` | none | list of `EmailAccount` | `email.accounts` / `email.accounts.result` | | `list` | `account` (text), optional `mailbox` (text), optional `messageId` (text), optional `cursor` (text), optional `limit` (integer) | `EmailPage` | `email.list` / `email.list.result` | | `resolve` | `locator` (text) | `EmailLocation` | `email.resolve` / `email.resolve.result` | | `get` | `account` (text), `message` (text) | `EmailMessage` | `email.get` / `email.get.result` | | `readPart` | `account` (text), `message` (text), `part` (text), `offset` (integer), `length` (integer) | `EmailBytes` | `email.readPart` / `email.readPart.result` | | `submit` | `account` (text), `submissionId` (text), `draft` (`EmailDraft`) | `EmailSubmission` | `email.submit` / `email.submit.result` | | `submission` | `account` (text), `submissionId` (text) | `EmailSubmission` | `email.submission` / `email.submission.result` | `accounts` returns only accounts the shell has authorized this napplet to discover. An empty list means none are exposed; it does not reveal whether other accounts exist. Account setup and grants occur through the shell's own UI. Every operation taking an account handle requires current discovery authorization as well as the operation's own grant. The shell MAY offer account selection or permission confirmation when handling a request. The napplet cannot preselect an undisclosed account by address. #### Common values | Value | Definition | | --- | --- | | Handles | Nonempty opaque text issued by the runtime. Account, message, part and cursor handles are not URLs, paths, protocol commands, or permission tokens. | | Locator | Nonempty opaque text identifying one immutable stored message entry within a shell user's account. Stable across authorized napplets and endpoint reloads in that runtime; not portable to another runtime or shell user. It conveys no authority. | | `submissionId` | Exactly 32 lowercase hexadecimal characters, generated randomly by the napplet for each deliberate new submission. It is separate from request correlation `id` and the email `Message-ID` header. | | Timestamp | Integer seconds since the Unix epoch, UTC. | | Encoded bytes | Canonical padded base64 with no whitespace. Byte counts and offsets refer to decoded bytes. | | Optional fields | Omission has the stated meaning. `null` is invalid except where explicitly permitted. Empty lists and empty text are valid only as specified. | | Control characters | Unicode U+0000–U+001F and U+007F–U+009F. | Requests MUST contain only their defined fields, including nested records; the runtime MUST reject extra fields with `invalid-request`. Results MAY add fields in later revisions; napplets MUST ignore unrecognized result fields. #### EmailAccount | Field | Required | Type | Notes | | --- | --- | --- | --- | | `account` | yes | text | Account handle scoped to the shell user and verified napplet identity. Stable across endpoint reloads while the account and identity remain the same. | | `label` | yes | text | Shell-selected display label; may be empty. No server or login details are required. | | `receive` | yes | boolean | Caller may request the runtime-held inbox for this account. | | `readSent` | yes | boolean | Caller may read account-level sent history, including submissions by other napplets. | | `send` | yes | boolean | Caller may request submission through this account. | | `senders` | yes | list of `EmailAddress` | Approved From identities. Nonempty when `send` is true; empty otherwise. | | `maxPageSize` | yes | integer | Positive maximum `list.limit`; omission of `limit` uses this value. | | `maxReadBytes` | yes | integer | Positive maximum `readPart.length`. | | `maxContentBytes` | yes | integer | Positive maximum sum of UTF-8 body bytes (`text` plus `html`, if present) and decoded attachment bytes per submission. | Inbox reading, sent-history reading, and sending are independent grants. `receive` and `send` reflect configured services and exposed grants; `readSent` exposes retained local history independently of current SMTP configuration. A send-only grant does not disclose inbox or other napplets' sent messages. A read grant does not imply a send grant. Limits are runtime-selected and MAY change; exceeding a current limit returns `too-large`. Additional finite resource and rate limits MAY apply. #### EmailAddress | Field | Required | Type | Notes | | --- | --- | --- | --- | | `address` | yes | text | One mailbox addr-spec under [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322.html); no display-name syntax, groups, comments, or control characters. | | `name` | no | text | Display name; omission means none. May be empty. MUST NOT contain control characters. | For outbound addresses the baseline is ASCII addr-spec syntax with an ASCII domain; internationalized local parts are outside this draft. Display names and message text MAY contain Unicode. The runtime MUST perform header encoding and validation; it MUST NOT concatenate napplet text into protocol commands. #### Contact selection A composer MAY integrate the `contacts` domain defined by [NAP-CONTACTS](https://github.com/napplet/naps/pull/140). It declares `contacts` as optional and checks availability through its projection. `contacts.pick` with `kind: "email"` requests trusted shell recipient selection; `multiple: true` allows several choices. Selecting contacts does not grant address-book browsing. Autocomplete or an address-book view uses `contacts.query` and `contacts.get` with `kind: "email"`, subject to their independent browse grants. The composer copies selected values into its own `EmailAddress` records: | Contact value | EmailAddress field | Rule | | --- | --- | --- | | Selected endpoint's `value` | `address` | Copy the email address as text; a contact handle is not an address. | | Selected contact's `name` | `name` | Optional display name; omit it when empty. | | Endpoint `label`, contact handle and source details | none | MUST NOT become mail headers, account choices, or recipient authority. | The napplet places those records in the draft's `to`, `cc`, or `bcc` list under the user's composition choices. Contact records and selection results are not embedded in email requests; `email` continues to own and validate `EmailAddress`. Email grants MUST NOT authorize contact access. Contact access MUST NOT authorize submission, select a sending account, or bypass email recipient restrictions or confirmation. The runtime MUST validate the resulting draft under this NAP even when its recipient values came from the shell's contact picker. When `contacts` is absent, denied, or unavailable, the composer MUST retain manual recipient entry. Cancellation or an empty picker result MUST leave the existing draft unchanged. Addresses from independently authorized mail reads can still be used for replies; contact browsing is not a prerequisite. #### EmailPage | Field | Required | Type | Notes | | --- | --- | --- | --- | | `messages` | yes | list of `EmailSummary` | At most `limit` items. Empty when the snapshot has no remaining items. | | `next` | yes | text or null | Opaque cursor for the next page, or `null` at the end. | | `syncedAt` | yes | integer or null | Completion time of the last successful POP3 retrieval reflected in an inbox or combined snapshot; `null` if none or for a sent-only snapshot. | `list.mailbox` is `"inbox"`, `"sent"`, or `"all"`; omission on a first page means `"inbox"`. Inbox access requires `receive`; sent access requires `readSent`; `"all"` requires both. The runtime MUST reject an unauthorized requested mailbox with `forbidden`, rather than silently return a subset. Sent history is shared at account scope under that grant, not partitioned by the submitting napplet. Optional `messageId` selects every retained entry whose parsed `messageId` matches exactly. It MUST be one valid RFC 5322 Message-ID including angle brackets. Comparison is case-sensitive, with no normalization inside the angle brackets. No match returns an empty page. Duplicate Message-IDs MUST NOT cause the runtime to merge entries or choose one as authoritative. The runtime schedules POP3 retrieval under shell policy; calling `list` does not force a network poll. A first page captures an immutable metadata snapshot, ordered by runtime ingestion time descending, then message handle in ascending Unicode code-point order. Subsequent pages preserve that order and `syncedAt`. New received or outgoing entries appear in a new first-page query. Expired cursors return `stale`; callers restart without a cursor. A cursor is bound to its caller scope, account, mailbox, Message-ID filter, and original limit. Explicit continuation parameters MUST equal their original values or return `invalid-request`; omitted parameters inherit those values. The runtime MUST recheck the original mailbox grants on every continuation. `limit` MUST be positive; a value above the current `maxPageSize` returns `too-large`. A reduced limit can require restarting pagination. #### EmailSummary and EmailMessage `EmailMessage` contains every `EmailSummary` field plus `parts`. | Field | Required | Type | Notes | | --- | --- | --- | --- | | `message` | yes | text | Opaque runtime message handle; scoped to the account. | | `locator` | yes | text | Stable locator for this stored entry. Distinct from correspondence Message-ID. | | `mailbox` | yes | "inbox" or "sent" | Store containing this entry. An incoming copy and an outgoing copy may be separate entries. | | `messageId` | no | text | Parsed Message-ID including angle brackets; omitted if absent or invalid. Untrusted correspondence metadata, not a unique runtime handle. | | `inReplyTo` | yes | list of text | Parsed In-Reply-To Message-IDs, in header order; empty if absent or invalid. | | `references` | yes | list of text | Parsed References Message-IDs, in header order; empty if absent or invalid. | | `from` | yes | list of `EmailAddress` | Parsed From mailboxes; empty if unavailable or invalid. | | `replyTo` | yes | list of `EmailAddress` | Parsed Reply-To mailboxes; empty if unavailable or invalid. A suggestion, not send authority. | | `to` | yes | list of `EmailAddress` | Parsed To mailboxes; empty if unavailable or invalid. | | `cc` | yes | list of `EmailAddress` | Parsed Cc mailboxes; empty if unavailable or invalid. | | `subject` | yes | text | Decoded subject; empty if unavailable. | | `date` | yes | integer or null | Parsed message Date; `null` if unavailable or invalid. Not trusted receipt time. | | `ingestedAt` | yes | integer | Time this entry first entered the inbox or sent history. | | `submissionState` | only for sent entries | "accepted" or "uncertain" | Outcome of the originating submission. Omitted for inbox entries. | | `parts` | only on EmailMessage | list of `EmailPart` | Non-multipart MIME entities in document order, including body alternatives and attachments. Multipart containers are traversed recursively; encapsulated messages remain opaque `message/rfc822` parts. | Header lists preserve mailbox order; groups in received address headers are flattened to their mailboxes. Invalid headers yield the empty or omitted values specified above. Header unfolding and encoded-word decoding follow [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322.html) and [RFC 2047](https://www.rfc-editor.org/rfc/rfc2047.html). Content-Type defaults follow MIME; `charset` preserves a declared charset rather than guessing one. Message-ID values retain the angle-bracketed identifier and omit surrounding comments or whitespace. Reply identifier lists preserve order and repetitions; the runtime MUST NOT invent identifiers or rewrite them into runtime handles. The runtime MUST preserve a handle's association with immutable message content until that content is removed. Removed content returns `not-found`; a handle MUST NOT be reassigned to different content. POP3 session message numbers and email Message-ID values MUST NOT serve as message handles. If persistent POP3 identifiers are unavailable or unreliable, the runtime MUST preserve this association using its local store; it MAY ingest duplicates but MUST NOT return different content under an existing handle. See [RFC 1939](https://www.rfc-editor.org/rfc/rfc1939.html). #### EmailLocation and cross-napplet reads `resolve` maps a locator to the caller's own account and message handles. | Field | Required | Type | Notes | | --- | --- | --- | --- | | `account` | yes | text | Caller-scoped account handle, as returned by `accounts`. | | `message` | yes | text | Caller-scoped message handle for `get` and `readPart`. | | `mailbox` | yes | "inbox" or "sent" | Determines the required read grant. | The runtime MUST require the receiver's own account-discovery and corresponding mailbox read grants. Missing, removed, wrong-user, and unauthorized locators all return `not-found` without account or message details. A locator MUST NOT grant access, transfer the sender's grants, or select another shell user. `get` and `readPart` MUST independently recheck the resolved mailbox grant. Locators MUST NOT be reassigned after retention removes an entry. A locator returned in a submission receipt does not give a send-only composer read access. #### Thread views and composition An inbox napplet can list messages; a reader can resolve a selected locator and read its parts; a composer can submit a reply; a thread napplet can page through `mailbox: "all"` and group retained entries using `messageId`, `inReplyTo`, and `references`. Exact Message-ID filtering supports fetching retained ancestors. The runtime supplies message facts; grouping, ordering within a conversation, reply choices, and treatment of missing parents remain napplet decisions. The runtime MUST NOT treat a reply identifier as proof of sender identity, conversation membership, permission, or uniqueness. Thread views must tolerate forged identifiers, duplicates, cycles, and missing or pruned messages. This surface promises access to authorized retained entries, not a complete history of all mail exchanged through the account. Napplets MAY exchange locators through separately agreed conventions. Such message semantics are outside this NAP; a locator is a runtime-provided data reference, not a handler choice or an authorization grant. Account and message handles scoped to one napplet MUST NOT be used as another napplet's handles. #### EmailPart and EmailBytes The runtime parses MIME under [RFC 2045](https://www.rfc-editor.org/rfc/rfc2045.html) and [RFC 2046](https://www.rfc-editor.org/rfc/rfc2046.html). It returns decoded content bytes, not transfer-encoded MIME bodies. | EmailPart field | Required | Type | Notes | | --- | --- | --- | --- | | `part` | yes | text | Handle bound to this immutable message. | | `mediaType` | yes | text | Lowercase media type without parameters, e.g. `text/plain`. | | `charset` | no | text | Declared text charset, if present. Omission does not assert UTF-8. | | `disposition` | yes | "inline" or "attachment" | Missing or unrecognized MIME disposition maps to `inline`; this is a presentation hint. | | `filename` | no | text | Decoded, untrusted suggested filename. Not a host path. | | `contentId` | no | text | Parsed Content-ID including angle brackets, if present. Local MIME reference only. | | `size` | yes | integer | Nonnegative decoded byte count. | Malformed MIME or transfer encoding returns `malformed-message` from `get` or `readPart`; it MUST NOT be executed or silently represented as a different message. Metadata can still appear in `list`. Unsupported text charsets MAY remain bytes; the napplet decides how to display them. | EmailBytes field | Required | Type | Notes | | --- | --- | --- | --- | | `offset` | yes | integer | Requested zero-based offset, from zero through the part's `size`. | | `data` | yes | text | Base64 for the next `min(length, size - offset)` bytes. Empty only at end. | | `eof` | yes | boolean | True exactly when the returned range ends at `size`. | `length` MUST be positive; exceeding `maxReadBytes` returns `too-large`. An offset below zero or beyond `size` is invalid. Chunking changes no content or part handles. If retention removes a message between reads, later reads return `not-found`. #### EmailDraft and EmailAttachment | EmailDraft field | Required | Type | Notes | | --- | --- | --- | --- | | `from` | yes | text | Exact `address` of one advertised sender; shell supplies its approved display name. | | `to` | yes | list of `EmailAddress` | May be empty if another recipient list is nonempty. | | `cc` | no | list of `EmailAddress` | Default empty. | | `bcc` | no | list of `EmailAddress` | Default empty. Envelope recipients only; MUST NOT appear in submitted message headers. | | `subject` | yes | text | May be empty. MUST NOT contain control characters. | | `text` | yes | text | UTF-8 plain-text body; may be empty. | | `html` | no | text | UTF-8 HTML alternative; omission means none. May be empty. | | `inReplyTo` | no | text | One valid RFC 5322 Message-ID including angle brackets. | | `references` | no | list of text | Valid RFC 5322 Message-IDs in caller order. Default empty. | | `attachments` | no | list of `EmailAttachment` | Default empty; sent as MIME attachments. | At least one recipient across `to`, `cc`, and `bcc` is required. The SMTP envelope is their union, deduplicated by exact address text; names do not affect that union. The runtime MUST validate the whole draft and all recipient permissions before admitting a submission; a grant MAY still require confirmation before the SMTP attempt. It MUST use the selected approved sender for From and the account's shell-configured envelope sender. It constructs Date, Message-ID, MIME headers, and body encoding. Reply metadata grants no account, recipient, or conversation authority. Local drafts stay with the napplet until submitted; this operation does not create a server-side draft or sent copy. The runtime MUST freeze the constructed message and envelope before requesting confirmation or starting an attempt. A change requires a new submission identifier. | EmailAttachment field | Required | Type | Notes | | --- | --- | --- | --- | | `filename` | yes | text | Nonempty suggested filename; no control characters or path separators. | | `mediaType` | yes | text | Lowercase valid media type without parameters. | | `data` | yes | text | Base64 attachment bytes; empty attachments are valid. | Attachment bytes are supplied explicitly. A path, URL, MIME reference, or received part handle MUST NOT cause the runtime to fetch or attach content. Forwarding a received attachment requires independently authorized reads and a new submission. #### EmailSubmission | Field | Required | Type | Notes | | --- | --- | --- | --- | | `submissionId` | yes | text | Caller-supplied submission identifier. | | `state` | yes | "pending" or "accepted" or "failed" or "uncertain" | Defined below. | | `messageId` | yes | text | Runtime-generated RFC 5322 Message-ID, fixed when the record is created. | | `locator` | only when accepted or uncertain | text | Locator of the corresponding retained sent-history entry. It may later expire independently of this receipt. | | `failure` | only when failed | `EmailError` | Definitive reason the message was not accepted for submission. | | State | Meaning | | --- | --- | | `pending` | Durably admitted; waiting for mediation or for one SMTP attempt to finish. No delivery claim. | | `accepted` | SMTP server positively acknowledged the complete message for every envelope recipient. No claim of final mailbox delivery or reading. | | `failed` | Definitively no message accepted for delivery to any recipient. No automatic retry. | | `uncertain` | An attempt may have succeeded but no definitive completion is available. No automatic retry. | The runtime MUST durably record a new submission before any SMTP attempt and before returning success from `submit`. It MAY return `pending` or a state already reached. `submission` reads the current record without causing an attempt. `pending` MUST eventually become a terminal state under a finite runtime-selected deadline. Terminal states MUST NOT change. Before an SMTP attempt, the runtime MUST durably retain the frozen message and reserve space for its sent-history entry. Failure to reserve space MUST prevent the attempt and produce `failed` with `busy`. On `accepted` or `uncertain`, the runtime MUST atomically persist the terminal outcome and one account-level sent entry before exposing that outcome. The entry contains the constructed message, its parts, reply metadata, and `submissionState`; its Message-ID equals the receipt's `messageId`. An uncertain entry MUST NOT be presented as proof of successful submission. A repeated `submit` MUST NOT create another history entry. Pending and failed submissions MUST NOT appear in sent history. Bcc envelope recipients, private submission identifiers, and submitting-napplet identity MUST NOT be added to the shared message metadata or MIME parts. History entry retention MAY later remove its content independently of receipt retention; its locator then returns `not-found`. Archiving never creates another SMTP attempt or marks an incoming copy as sent. Submission identity is scoped to shell user, verified napplet identity, and account. Repeating `submit` with the same `submissionId` and same draft MUST return the existing record without another SMTP attempt or confirmation. Different drafts under an existing identifier return `conflict`. Equality ignores record-field order and applies the defaults in the schema; list order and text remain significant. Requests using different correlation ids do not create different submissions. A new deliberate attempt requires a new `submissionId` and fresh policy evaluation. The runtime MUST reserve new identifiers atomically across concurrent requests in the same scope. Existing records and tombstones are resolved after authority and schema checks, before applying limits for a new admission. A later limit change MUST NOT turn an identical repeat into another attempt. The runtime MUST preserve records across endpoint disconnects and runtime restarts. It MAY discard terminal record details under shell retention policy, but MUST retain a tombstone preventing reuse of that identifier for the account's lifetime. Queries and repeated submits for tombstones return `expired`, with no SMTP attempt. If it cannot reserve durable state, it MUST reject before effects. Removing and recreating an account MUST issue a different account handle. The runtime MUST make at most one SMTP attempt per admitted identifier. It MUST abort before transmitting the message body if any envelope recipient is rejected; partial recipient submission is not supported. A positive final acknowledgment produces `accepted`; a definitive rejection produces `failed`. Loss of the final response or a restart after an attempt may have begun produces `uncertain` unless the runtime has definitive evidence. Recovery MUST NOT resubmit an ambiguous attempt. This prevents runtime retries from duplicating an uncertain send; SMTP itself offers no exactly-once delivery guarantee. See [RFC 5321](https://www.rfc-editor.org/rfc/rfc5321.html). ### Wire Protocol Messages use the projection's carrier. The following logical fields define the contract; no host transport is required here. | Type | Direction | Payload fields | | --- | --- | --- | | `email.accounts` | napplet → runtime | `id` | | `email.accounts.result` | runtime → napplet | `id`, `value` or `error` | | `email.list` | napplet → runtime | `id`, `account`, `mailbox?`, `messageId?`, `cursor?`, `limit?` | | `email.list.result` | runtime → napplet | `id`, `value` or `error` | | `email.resolve` | napplet → runtime | `id`, `locator` | | `email.resolve.result` | runtime → napplet | `id`, `value` or `error` | | `email.get` | napplet → runtime | `id`, `account`, `message` | | `email.get.result` | runtime → napplet | `id`, `value` or `error` | | `email.readPart` | napplet → runtime | `id`, `account`, `message`, `part`, `offset`, `length` | | `email.readPart.result` | runtime → napplet | `id`, `value` or `error` | | `email.submit` | napplet → runtime | `id`, `account`, `submissionId`, `draft` | | `email.submit.result` | runtime → napplet | `id`, `value` or `error` | | `email.submission` | napplet → runtime | `id`, `account`, `submissionId` | | `email.submission.result` | runtime → napplet | `id`, `value` or `error` | `id` is nonempty text, unique among the calling endpoint's outstanding requests. For each authenticated, routable request with a valid correlation id, the runtime MUST return exactly one corresponding result with the same `id` to that endpoint. Success contains `value` of the API row's result type and no `error`; failure contains `error` and no `value`. Correlation is endpoint-scoped. There are no unsolicited deliveries or wire subscriptions; callers poll inbox and sent-history snapshots and submission records subject to shell rate limits. #### Error Handling `EmailError`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `code` | yes | text | One of the codes below. Callers branch on this value. | | `message` | yes | text | Safe human-readable explanation. | | Code | Meaning | | --- | --- | | `invalid-request` | Invalid schema, field, encoding, syntax, or range. | | `forbidden` | Current policy denies account discovery or the requested account, data, sender, or recipients. | | `unsupported` | Account does not expose the required configured service. | | `not-found` | Message, part, locator, or submission is absent in the caller's authorized scope. | | `stale` | Cursor snapshot expired. | | `expired` | Submission details expired; its identifier remains reserved. | | `conflict` | Existing submission identifier was used with a different draft. | | `too-large` | Request or result exceeds a runtime limit. | | `busy` | Finite work, rate, or durable-storage capacity is exhausted. | | `unavailable` | Required account service or local store is unavailable, including authentication or TLS failure. | | `malformed-message` | Stored message cannot be safely parsed or decoded. | | `rejected` | SMTP server definitively rejected the attempt; used in a failed submission's `failure`. | The runtime MUST validate and authorize before effects. Unknown and unauthorized account handles both return `forbidden`. Within an authorized account, unknown and out-of-scope message, part, cursor, and submission identifiers MUST be indistinguishable from absent identifiers (`not-found`). Policy checks precede existence disclosure. A missing service returns `unsupported` only after account access is established. An invalid range on an authorized existing part returns `invalid-request`. Rate and size errors require no disclosure of hidden data. Locator resolution follows its indistinguishable `not-found` rule even when the receiver has no grant for the locator's account. Message-ID filtering does not widen mailbox grants, and an empty match reveals nothing about other stores. After admission, an SMTP failure is a successful API result carrying a `failed` submission record. API failure MUST NOT report a definitive no-send outcome for an admitted attempt. If the reply is lost, the napplet SHOULD query `submission` or repeat the identical `submit`; it MUST NOT infer failure and automatically choose a new identifier. `not-found` after a timeout is not proof that no earlier request can still be admitted; repeating the identical request remains safe. #### Examples **Discover accounts already exposed by shell policy:** ```text → { "type": "email.accounts", "id": "a1" } ← { "type": "email.accounts.result", "id": "a1", "value": [{ "account": "a1", "label": "Personal", "receive": true, "send": true, "senders": [{ "address": "bob@example.net" }], "maxPageSize": 100, "maxReadBytes": 65536, "maxContentBytes": 10485760, "readSent": true }] } ``` **Read an authorized account's inbox and a body part:** ```text → { "type": "email.list", "id": "l1", "account": "a1", "limit": 10 } ← { "type": "email.list.result", "id": "l1", "value": { "messages": [{ "message": "m1", "from": [{ "address": "alice@example.org" }], "replyTo": [], "to": [{ "address": "bob@example.net" }], "cc": [], "subject": "Hello", "date": null, "ingestedAt": 1793491200, "locator": "loc-in-1", "mailbox": "inbox", "messageId": "", "inReplyTo": [], "references": [] }], "next": null, "syncedAt": 1793491200 } } → { "type": "email.get", "id": "g1", "account": "a1", "message": "m1" } ← { "type": "email.get.result", "id": "g1", "value": { "message": "m1", "from": [{ "address": "alice@example.org" }], "replyTo": [], "to": [{ "address": "bob@example.net" }], "cc": [], "subject": "Hello", "date": null, "ingestedAt": 1793491200, "parts": [{ "part": "p1", "mediaType": "text/plain", "charset": "utf-8", "disposition": "inline", "size": 5 }], "locator": "loc-in-1", "mailbox": "inbox", "messageId": "", "inReplyTo": [], "references": [] } } → { "type": "email.readPart", "id": "r1", "account": "a1", "message": "m1", "part": "p1", "offset": 0, "length": 5 } ← { "type": "email.readPart.result", "id": "r1", "value": { "offset": 0, "data": "SGVsbG8=", "eof": true } } ``` **Invalid range and denied account; neither grants access or changes mail:** ```text → { "type": "email.readPart", "id": "r2", "account": "a1", "message": "m1", "part": "p1", "offset": -1, "length": 5 } ← { "type": "email.readPart.result", "id": "r2", "error": { "code": "invalid-request", "message": "Offset must be nonnegative." } } → { "type": "email.list", "id": "l2", "account": "not-granted" } ← { "type": "email.list.result", "id": "l2", "error": { "code": "forbidden", "message": "Account access is not authorized." } } ``` **Admission, retry after a lost reply, and uncertain SMTP completion:** ```text → { "type": "email.submit", "id": "s1", "account": "a1", "submissionId": "3ca28f17da2648b3a0ad1e0281c5f647", "draft": { "from": "bob@example.net", "to": [{ "address": "alice@example.org" }], "subject": "Re: Hello", "text": "Hello", "inReplyTo": "", "references": [""] } } ← { "type": "email.submit.result", "id": "s1", "value": { "submissionId": "3ca28f17da2648b3a0ad1e0281c5f647", "state": "pending", "messageId": "" } } → { "type": "email.submit", "id": "s2", "account": "a1", "submissionId": "3ca28f17da2648b3a0ad1e0281c5f647", "draft": { "from": "bob@example.net", "to": [{ "address": "alice@example.org" }], "subject": "Re: Hello", "text": "Hello", "inReplyTo": "", "references": [""] } } ← { "type": "email.submit.result", "id": "s2", "value": { "submissionId": "3ca28f17da2648b3a0ad1e0281c5f647", "state": "pending", "messageId": "" } } → { "type": "email.submission", "id": "s3", "account": "a1", "submissionId": "3ca28f17da2648b3a0ad1e0281c5f647" } ← { "type": "email.submission.result", "id": "s3", "value": { "submissionId": "3ca28f17da2648b3a0ad1e0281c5f647", "state": "uncertain", "messageId": "", "locator": "loc-out-1" } } ``` Both submits refer to one durable record and at most one SMTP attempt. With a definitive positive final SMTP response, the queried state would be `accepted`. Reusing the identifier with changed text returns `conflict`. If the endpoint disconnects after admission, the runtime keeps the record; a new authorized endpoint of the same verified napplet identity can query it. A different napplet using the same correlation id receives only its own result, and cannot inspect this record by copying these handles. **Another napplet lists the two-sided retained conversation:** The thread napplet has its own account handle `thread-a1` and both mailbox read grants. The outgoing entry comes from the composer above; its uncertainty is preserved. The complete snapshot can be paginated to discover descendants; an exact `messageId` filter can retrieve particular referenced ancestors. ```text → { "type": "email.list", "id": "t1", "account": "thread-a1", "mailbox": "all", "limit": 10 } ← { "type": "email.list.result", "id": "t1", "value": { "messages": [{ "message": "thread-out-1", "locator": "loc-out-1", "mailbox": "sent", "messageId": "", "inReplyTo": [""], "references": [""], "from": [{ "address": "bob@example.net" }], "replyTo": [], "to": [{ "address": "alice@example.org" }], "cc": [], "subject": "Re: Hello", "date": 1793491260, "ingestedAt": 1793491260, "submissionState": "uncertain" }, { "message": "thread-in-1", "locator": "loc-in-1", "mailbox": "inbox", "messageId": "", "inReplyTo": [], "references": [], "from": [{ "address": "alice@example.org" }], "replyTo": [], "to": [{ "address": "bob@example.net" }], "cc": [], "subject": "Hello", "date": null, "ingestedAt": 1793491200 }], "next": null, "syncedAt": 1793491200 } } → { "type": "email.list", "id": "t2", "account": "thread-a1", "mailbox": "all", "messageId": "" } ← { "type": "email.list.result", "id": "t2", "value": { "messages": [{ "message": "thread-in-1", "locator": "loc-in-1", "mailbox": "inbox", "messageId": "", "inReplyTo": [], "references": [], "from": [{ "address": "alice@example.org" }], "replyTo": [], "to": [{ "address": "bob@example.net" }], "cc": [], "subject": "Hello", "date": null, "ingestedAt": 1793491200 }], "next": null, "syncedAt": 1793491200 } } ``` **Resolve a handoff using the receiving reader's own grants:** A separately authorized reader receives `loc-out-1`, not the composer's private account handle or submission identifier. Its handles can differ from the thread napplet's handles while identifying the same stored entry. ```text → { "type": "email.resolve", "id": "h1", "locator": "loc-out-1" } ← { "type": "email.resolve.result", "id": "h1", "value": { "account": "reader-a1", "message": "reader-out-1", "mailbox": "sent" } } → { "type": "email.get", "id": "h2", "account": "reader-a1", "message": "reader-out-1" } ← { "type": "email.get.result", "id": "h2", "value": { "message": "reader-out-1", "locator": "loc-out-1", "mailbox": "sent", "messageId": "", "inReplyTo": [""], "references": [""], "from": [{ "address": "bob@example.net" }], "replyTo": [], "to": [{ "address": "alice@example.org" }], "cc": [], "subject": "Re: Hello", "date": 1793491260, "ingestedAt": 1793491260, "submissionState": "uncertain", "parts": [{ "part": "reader-p1", "mediaType": "text/plain", "charset": "utf-8", "disposition": "inline", "size": 5 }] } } ``` A napplet without the account's sent-history grant receives the same result as for a missing locator. The submitting composer's send grant cannot be reused: ```text → { "type": "email.resolve", "id": "h3", "locator": "loc-out-1" } ← { "type": "email.resolve.result", "id": "h3", "error": { "code": "not-found", "message": "Message is not available in this scope." } } ``` ### Shell Behavior - The shell MUST configure and store accounts outside napplet-controlled UI and schemas. It MUST NOT accept server, port, credential, TLS, or identity overrides from a napplet request. The user can configure SMTP, POP3, or both and manage grants through the shell without running a particular email napplet. - The runtime MUST derive caller scope from the authenticated endpoint's verified manifest and artifact identity, as defined by the projection, plus the shell user. A payload's address, pubkey, manifest claim, or handle cannot replace that binding. Grants MUST distinguish account discovery, reading inbox mail, reading sent history, and sending mail. New artifacts require shell policy evaluation. - The shell MUST disclose the scope of each read grant: headers, bodies, and attachments in the selected store. Sent-history reading includes mail submitted by all napplets using the account; it excludes private drafts and submission receipts. The shell MUST disclose account-level retention and history sharing when authorizing sending and allow the user to manage retained history. The runtime MUST enforce the appropriate mailbox read authority on `list`, `resolve`, `get`, and `readPart`, and caller-private send authority on `submit` and `submission`. Sending alone MUST NOT authorize sent-history reading. The shell MAY require confirmation for each draft or authorize bounded sending in advance. Any confirmation MUST cover the sender, every recipient including Bcc, message body alternatives, and attachments of the immutable admitted draft. - The runtime MUST recheck policy before an SMTP attempt. Revocation before the attempt or declined confirmation produces `failed` with `forbidden`. Revocation or disconnect after an attempt begins cannot promise recall; the runtime MUST preserve its outcome internally. Revocation MUST stop later disclosures and unstarted attempts. In-flight reads MUST recheck authority before releasing their results. On a shell-user switch, the runtime MUST NOT deliver the old user's results to the new user. Endpoint teardown stops delivery, not durable submission tracking. - For configured POP3 accounts, the shell MUST provide retrieval controls and show the last successful retrieval and current failures, including login or TLS failure. The runtime SHOULD retrieve periodically while the account is enabled, under user-selected shell policy. Reading a cached inbox MUST NOT imply that the POP3 server is reachable or synchronized at request time. - The runtime MUST bound retrieval, MIME nesting, decoded bytes, pagination, message retention, submissions, and request duration. Limit failures use `too-large`, `busy`, or `unavailable` as applicable. An admitted submission's deadline follows its state rules rather than erasing an ambiguous attempt. - The runtime MAY use local libraries, native services, or a trusted configured backend. Backend selection does not change this contract. A remote backend handling credentials or plaintext mail is part of the runtime's trust boundary; the shell MUST disclose that handling to the user during account setup. ### Security Considerations - The runtime MUST keep credentials, authentication transcripts, tokens, server settings and login identifiers out of napplet results, logs exposed to napplets, and error text. Approved sender addresses are deliberate disclosures; an address does not establish a Nostr identity or prove an incoming author. - The runtime MUST use authenticated TLS for SMTP submission and POP3 access, consistent with [RFC 8314](https://www.rfc-editor.org/rfc/rfc8314.html). It MUST reject certificate or required TLS negotiation failures and MUST NOT silently fall back to plaintext. Credentials MUST only reach the configured authorized service. Mail headers, attachments and napplet input MUST NOT redirect account connections or choose authentication mechanisms. - Email content, sender names, MIME metadata, HTML and filenames are untrusted. The runtime MUST parse them with bounded work and MUST NOT execute scripts, resolve external entities, load remote images, or follow content URLs while retrieving or parsing mail. Napplets rendering HTML MUST keep it inert and sanitize it; a MIME media type conveys no execution authority. The projection's isolation and network restrictions still apply to rendered mail. - Possession of a read grant permits disclosure of private mail to that napplet. The shell MUST NOT describe it as a display-only grant. It does not authorize uploading mail, sending replies, or retrieving external tracking content; those effects require their own applicable shell mediation. - The runtime MUST prevent header and command injection, Bcc header disclosure, filename traversal, decompression bombs and cross-account cache leakage. It MUST NOT save attachment filenames as host paths without independent handling. Submission confirmation MUST show the exact authorized draft, not a mutable napplet-controlled preview. ### Implementations No implementation of this NAP is verified. Existing SMTP and POP3 clients do not establish support for this draft. ### Changelog - `51bce7f` - Defined shell-configured SMTP/POP3 accounts, scoped inbox and MIME access, structured submission, durable retry outcomes, and email authority boundaries. - `2ee6cd8` - Added incoming reply metadata, account-scoped sent history with separate grants, filtered mailbox queries, and authorized cross-napplet locator resolution. - `85f76e1` - Declared optional contacts integration for recipient selection and separately granted browsing, with explicit address mapping, independent authority, and manual-entry fallback. [Join the discussion · PR #139 ↗](https://github.com/napplet/naps/pull/139)[Read proposed changes ↗](https://github.com/napplet/naps/pull/139/files) On this pageSpecification metadata Capability domain `email` Repository state Draft PR Registry status Not listed in the registry Document marker draft Declared dependencies - `contacts` Source revision `a63c592c78eb` [View source on GitHub ↗](https://github.com/napplet/naps/pull/139/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-FS / PR #88 # Virtual Filesystem Access Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** Filesystem binding has MVP coverage; portability and authorization across storage backends remain under evaluation. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/88), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/hzrd149/naps/blob/bd1b9e912f05203bf38f7a1b04f920744fe6d625/naps/NAP-FS.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/88/files). ## NAP-FS ### Virtual Filesystem Access `draft` **NAP ID:** NAP-FS **Domain:** `fs` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.fs`; domain presence signals availability. ### Description NAP-FS provides napplets with shell-mediated access to a runtime-owned virtual filesystem. The filesystem may be backed by local files, origin-private storage, remote storage, a database, or any other runtime implementation. A napplet sees only virtual paths, directory entries, file metadata, byte reads and writes, user-mediated picker results, and advisory change events. Backing storage can be shared across napplets. Each napplet still sees a policy-bound view with its own permissions. Every operation is authorized against the runtime-bound napplet identity. Paths, permission lists, metadata, and options supplied by the napplet are not authority. Only runtime policy and runtime-mediated user choice can expand the visible view. NAP-FS defines the filesystem seam. It does not define host paths, mount sources, picker UI, sync backends, OS permissions, or storage layout. Picker operations return only virtual paths in the napplet's visible filesystem. ### Path Model Paths are virtual absolute paths in the napplet's visible filesystem. - A path MUST start with `/`. - `/` is the only path separator. - `/` identifies the visible virtual root. - Empty segments are invalid except for the root `/`. - `.` and `..` segments are invalid. - Host drive prefixes, file URLs, and OS-native separators are invalid. - Path segments are Unicode strings, not URLs. A runtime MUST NOT percent-decode a path or interpret lookalike characters as `/`. - Control characters and bidirectional formatting characters are invalid. - A runtime MUST apply one consistent Unicode normalization and case policy before authorization. Creation and move MUST fail with `conflict` when the destination collides with an entry under that policy. - A runtime MUST normalize and validate paths before authorization and MUST revalidate the normalized path immediately before committing a mutation. - A runtime MUST resolve any backing-store aliases, links, and mounts before authorization. It MUST authorize the final objects and revalidate them immediately before committing a mutation. - Authorization MUST compare parsed path segments against policy roots. String prefix comparison is insufficient: `/shared/app` does not contain `/shared/app-private`. - A runtime MUST reject any path that escapes the napplet's authorized view. ### Shared Filesystem Policy A shared backing store does not imply shared write access. Read, list, write, create, delete, and watch permissions are independent. A runtime that exposes a root mutable by multiple napplets MUST apply explicit policy to every mutation. - Shared readable roots MUST NOT become shared writable roots by implication. - A napplet MUST NOT mutate an entry outside its authorized mutable scope, even when it can read or list the entry. - Overwrite, recursive removal, and move in a shared mutable root MUST be explicitly allowed by runtime policy. The runtime MAY require user mediation. - Authorization MUST use current runtime policy and the runtime-bound napplet identity. Entry names, metadata, and request options are not authority. - A mutation that cannot prevent unauthorized effects across its authorization and commit steps MUST fail with `conflict`, `policy-denied`, or `unsupported`. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `info` | none | `FsInfo` | `fs.info` / `fs.info.result` | | `pickFile` | optional `options` (`FsPickOptions`) | `FsPickResult` | `fs.pickFile` / `fs.pickFile.result` | | `pickFiles` | optional `options` (`FsPickOptions`) | `FsPickResult` | `fs.pickFiles` / `fs.pickFiles.result` | | `pickDirectory` | optional `options` (`FsPickOptions`) | `FsPickResult` | `fs.pickDirectory` / `fs.pickDirectory.result` | | `pickSaveFile` | optional `options` (`FsPickOptions`) | `FsPickResult` | `fs.pickSaveFile` / `fs.pickSaveFile.result` | | `stat` | `path` (text) | `FsMetadata` | `fs.stat` / `fs.stat.result` | | `list` | `path` (text) | list of `FsDirectoryEntry` | `fs.list` / `fs.list.result` | | `read` | `path` (text), optional `options` (`FsReadOptions`) | `FsReadResult` | `fs.read` / `fs.read.result` | | `write` | `path` (text), `data` (text, base64-encoded bytes), optional `options` (`FsWriteOptions`) | `FsWriteResult` | `fs.write` / `fs.write.result` | | `mkdir` | `path` (text), optional `options` (`FsMkdirOptions`) | none | `fs.mkdir` / `fs.mkdir.result` | | `remove` | `path` (text), optional `recursive` (boolean) | none | `fs.remove` / `fs.remove.result` | | `move` | `fromPath` (text), `toPath` (text) | none | `fs.move` / `fs.move.result` | | `watch` | `path` (text), optional `options` (`FsWatchOptions`) | `watchId` (text) | `fs.watch` / `fs.watch.result` | | `unwatch` | `watchId` (text) | none | `fs.unwatch` / `fs.unwatch.result` | #### Schemas `FsPermission` values: | Value | Notes | | --- | --- | | `"read"` | Read file bytes. | | `"write"` | Write existing file bytes. | | `"create"` | Create new entries. | | `"delete"` | Remove entries. | | `"list"` | List directory entries. | | `"watch"` | Watch visible changes. | `FsEntryKind` values: | Value | Notes | | --- | --- | | `"file"` | File entry. | | `"directory"` | Directory entry. | | `"unknown"` | Runtime cannot safely expose file or directory semantics. | `FsWriteMode` values: | Value | Notes | | --- | --- | | `"replace"` | Replace the whole file. | | `"append"` | Append to the file. | | `"patch"` | Write at an offset. | `FsChangeKind` values: | Value | Notes | | --- | --- | | `"created"` | Entry was created. | | `"modified"` | Entry was modified. | | `"deleted"` | Entry was deleted. | | `"moved"` | Entry was moved. | | `"unknown"` | Runtime cannot classify the change. | `FsError` values: | Value | Notes | | --- | --- | | `"not-found"` | Entry was absent or hidden. | | `"already-exists"` | Destination already exists. | | `"not-a-file"` | Operation requires a file. | | `"not-a-directory"` | Operation requires a directory. | | `"invalid-path"` | Path violates the path model. | | `"invalid-data"` | Byte payload is malformed. | | `"permission-denied"` | Permission is absent. | | `"policy-denied"` | Runtime policy denies the operation. | | `"quota-exceeded"` | Storage quota would be exceeded. | | `"too-large"` | Request exceeds a size limit. | | `"unsupported"` | Runtime does not support the requested behavior. | | `"conflict"` | Precondition, concurrency, or mutation conflict. | | `"cancelled"` | User or runtime cancelled the operation. | | `"io-error"` | Backing store failed. | `FsInfo` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `roots` | yes | list of `FsRoot` | Visible roots. | | `limits` | yes | `FsLimits` | Runtime limits. | `FsRoot` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `path` | yes | text | Virtual absolute root path. | | `name` | yes | text | Runtime-curated display label. | | `permissions` | yes | list of `FsPermission` | Coarse permissions advertised for this root. | | `description` | no | text | Runtime-curated description. | `FsLimits` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `maxReadBytes` | yes | integer | Non-negative. Maximum decoded bytes requested by one `read`. | | `maxWriteBytes` | yes | integer | Non-negative. Maximum decoded bytes supplied by one `write`. | | `maxWatchCount` | no | integer | Non-negative. Maximum active watches. | | `maxInFlightRequests` | no | integer | Non-negative. Maximum active requests. | | `maxInFlightBytes` | no | integer | Non-negative. Maximum aggregate decoded bytes in active byte operations. | `FsAcceptRule` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `mime` | no | text | Advisory MIME filter. | | `extension` | no | text | Advisory extension filter. | `FsPickOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `permissions` | no | list of `FsPermission` | Requested permission intent. | | `accept` | no | list of `FsAcceptRule` | Advisory UI filters. | | `suggestedName` | no | text | Advisory save name. | | `description` | no | text | Advisory picker description. | `FsPickedEntry` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `path` | yes | text | Virtual absolute path. | | `kind` | yes | `"file"` or `"directory"` | Picked entry kind. | | `name` | yes | text | Entry name. | | `permissions` | yes | list of `FsPermission` | Permissions granted for the picked entry. | | `size` | no | integer | Non-negative. Size in bytes. | | `modifiedAt` | no | integer | Non-negative. Runtime-defined timestamp. | `FsPickResult` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `entries` | yes | list of `FsPickedEntry` | Picked entries. | `FsMetadata` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `path` | yes | text | Virtual absolute path. | | `kind` | yes | `FsEntryKind` | Entry kind. | | `size` | no | integer | Non-negative. Size in bytes. | | `modifiedAt` | no | integer | Non-negative. Runtime-defined timestamp. | | `createdAt` | no | integer | Non-negative. Runtime-defined timestamp. | | `permissions` | no | list of `FsPermission` | Coarse permissions known for this entry. | | `revision` | no | text | Opaque write-precondition token. | `FsDirectoryEntry` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `name` | yes | text | Entry name. | | `path` | yes | text | Virtual absolute path. | | `kind` | yes | `FsEntryKind` | Entry kind. | | `size` | no | integer | Non-negative. Size in bytes. | | `modifiedAt` | no | integer | Non-negative. Runtime-defined timestamp. | `FsReadOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `offset` | no | integer | Non-negative. Starting byte offset. | | `length` | no | integer | Non-negative. Requested decoded byte count. | `FsReadResult` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `data` | yes | text | Decoded file bytes, encoded as standard padded base64 on the JSON wire. | | `offset` | yes | integer | Non-negative. Starting byte offset. | | `bytesRead` | yes | integer | Non-negative. Count of decoded bytes in `data`. | | `eof` | yes | boolean | Whether no more bytes are available after this result. | | `size` | no | integer | Non-negative. Total file size in bytes. | `FsWriteOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `mode` | no | `FsWriteMode` | Write mode. | | `offset` | no | integer | Non-negative. Patch byte offset. | | `ifRevision` | no | text | Opaque revision precondition. | | `ifAbsent` | no | boolean | Create-only precondition when true. | `FsWriteResult` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `bytesWritten` | yes | integer | Non-negative. Count of decoded bytes committed. | | `size` | no | integer | Non-negative. Resulting file size in bytes. | `FsMkdirOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `recursive` | no | boolean | Whether missing parent directories may be created. | `FsWatchOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `recursive` | no | boolean | Whether directory watches include visible descendants. | `FsChange` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `watchId` | yes | text | Runtime-generated watch id. | | `path` | yes | text | Virtual absolute path. | | `kind` | yes | `FsChangeKind` | Change kind. | | `fromPath` | no | text | Previous virtual path for moved entries. | **`info()`** — Returns visible roots, coarse root permissions, and runtime limits. This is advisory discovery, not authorization. Permissions can change during a session, a root can contain paths with narrower policy, and any operation can still fail. `info()` MUST NOT expose host paths, volume names, backing-store types, usernames, exact ACLs, free disk space, or runtime-internal mount identifiers. Root names and descriptions MUST be runtime-curated labels safe to disclose to the napplet. They MUST NOT reveal account names, device names, private folder names, storage providers, organizations, or user-specific labels unless runtime policy explicitly permits that disclosure. **`pickFile(options?)`** — Asks the runtime to let the user select one file. Picker operations are user-mediated authority requests. The napplet supplies intent and advisory filters only. It MUST NOT supply host paths, file URLs, OS-native paths, common-folder identifiers, volume names, mount identifiers, or storage-provider identifiers. The runtime MAY show UI, require a user gesture, deny the request, or return `cancelled` when the user cancels. A successful `pickFile` result MUST contain exactly one file entry. **`pickFiles(options?)`** — Asks the runtime to let the user select one or more files. A successful result MUST contain at least one file entry. Cancellation MUST be reported with `cancelled`, not as an empty successful result. **`pickDirectory(options?)`** — Asks the runtime to let the user select one directory. A successful result MUST contain exactly one directory entry. **`pickSaveFile(options?)`** — Asks the runtime to let the user select or name one file destination. A successful result MUST contain exactly one file entry. The entry MAY be absent in the backing store until the napplet writes it, but the returned virtual path MUST be authorized for the returned permissions. Picker options are hints, not authority. Requested permissions, accepted MIME types, accepted extensions, suggested names, and descriptions do not widen the napplet's view. The runtime decides the actual selected objects and permissions according to user choice and runtime policy. `accept` filters are advisory UI filters only; runtimes and napplets MUST NOT treat them as content validation. When `permissions` is absent, the requested permission intent defaults to `read` for `pickFile` and `pickFiles`, `read` and `list` for `pickDirectory`, and `write` and `create` for `pickSaveFile`. Selected files or directories MAY originate outside the napplet's current visible virtual filesystem. Before returning success, the runtime MUST expose each selected object through a virtual absolute path in the napplet's visible filesystem. The returned path MUST obey the NAP-FS path model. Subsequent `stat`, `list`, `read`, `write`, `mkdir`, `remove`, `move`, `watch`, and `unwatch` operations on that path use normal NAP-FS authorization. Picker results MUST NOT expose host paths, file URLs, OS-native separators, volume names, usernames, mount identifiers, backing-store identifiers, or storage provider identifiers. They MUST NOT reveal whether the selected object was already inside an existing visible root, mounted from outside it, copied into the filesystem, proxied, or backed by another runtime mechanism. Picker grants are not durable authority. A runtime MAY make a picked entry visible only for the current session, MAY persist it across sessions, or MAY revoke it at any time according to runtime policy. A napplet MUST NOT assume that a picked path remains available after remount, reload, or runtime restart. It MUST rediscover available roots with `info()` or ask the user to pick again. **`stat(path)`** — Returns metadata for a visible file or directory. Metadata is intentionally coarse. It omits host-specific identifiers such as inode, device, owner, group, mode bits, symlink target, and backing-store metadata. `revision`, when present, is an opaque token for write preconditions. Napplets MUST compare it only for equality and MUST NOT infer ordering or content from it. A runtime that exposes revisions MUST change the revision when file content changes. **`list(path)`** — Lists direct children of a visible directory. Result ordering is unspecified. Listing a file MUST fail with `not-a-directory`. **`read(path, options?)`** — Reads bytes from a file. Range reads are mandatory. `offset` defaults to `0`. `length` defaults to the runtime's maximum readable chunk. `length` counts decoded bytes and MUST NOT exceed `FsLimits.maxReadBytes`. The runtime MAY return fewer bytes than requested but, for a non-zero `length`, MUST return at least one byte or set `eof: true`. `eof: true` means no more bytes are available after this result. Reading a directory MUST fail with `not-a-file`. **`write(path, data, options?)`** — Writes bytes to a file. Range writes are mandatory. `mode` defaults to `replace`. `replace` replaces the whole file and MUST NOT carry `offset`. `append` appends to the file and MUST NOT carry `offset`. `patch` writes at `offset` and MUST carry `offset`. The decoded length of `data` MUST NOT exceed `FsLimits.maxWriteBytes`. A successful write MUST commit all supplied bytes atomically and report `bytesWritten` equal to the decoded data length. If the runtime cannot do so, it MUST fail without changing the file. Concurrent appends MUST NOT interleave their bytes. Writing an existing file requires `write` permission. Creating an absent file requires `create` permission and authorization on its parent directory. Runtime policy MAY require both permissions. `ifRevision`, when present, requires the current file revision to match. `ifAbsent: true` requires the path to be absent. `ifAbsent: false` has no effect. An unmet precondition MUST fail with `conflict` without changing the file. A runtime that does not expose revisions MUST reject `ifRevision` with `unsupported`. **`mkdir(path, options?)`** — Creates a directory. If `recursive` is absent or `false`, the parent directory MUST already exist. If `recursive: true`, the runtime creates missing parent directories within the napplet's authorized view. Recursive creation MUST NOT cross hidden parents, mount boundaries, or any path outside the napplet's authorized view. Creation of every missing directory requires `create` permission and authorization on its existing parent. **`remove(path, recursive?)`** — Removes a file or directory. Removing a non-empty directory without `recursive: true` MUST fail with `conflict`. Removal requires `delete` permission on the target. Recursive removal MUST be authorized for every visible entry it removes and MUST NOT remove hidden descendants outside the napplet's authorized view. Before deleting any entry, the runtime MUST determine the complete removal set from an authorized stable snapshot or use an equivalent mechanism that reauthorizes each entry immediately before removal. It MUST NOT remove an entry added or moved into the subtree unless that entry is separately authorized. If the tree changes during the operation and safe completion is uncertain, the runtime MUST fail with `conflict`. **`move(fromPath, toPath)`** — Moves or renames a file or directory. The runtime MUST atomically authorize the source entry, every descendant, the destination parent, and replacement of any destination entry. It MUST revalidate that authorization immediately before commit. Moving an entry MUST NOT escape the napplet's authorized view. Cross-root or mount-boundary moves MUST fail with `unsupported` or `policy-denied` unless runtime policy explicitly authorizes the complete operation. The source requires `delete` permission. An absent destination requires `create` permission and authorization on its parent. Replacing an existing destination requires `delete` permission on that entry. Runtime policy MAY require additional permissions. Policy attached to the moved entry MUST be preserved or re-evaluated before it becomes visible at the destination. A race or policy change MUST fail with `conflict` without moving the entry. If the destination exists, the runtime MAY reject with `already-exists` or `conflict` according to runtime policy. **`watch(path, options?)`** — Starts an advisory watch and returns `watchId`. `recursive` defaults to `false`. If `recursive: false`, directory watches cover the directory entry and direct children. If `recursive: true`, directory watches cover the directory and visible descendants. `recursive` has no effect for file watches. A runtime MAY reject recursive watches with `unsupported` or `policy-denied`. Watch permission is independent from read and list permission. Recursive watch requires explicit runtime policy. Watch events are invalidation signals. They MAY be coalesced, duplicated, reordered, dropped, or reported as `unknown`. Napplets SHOULD call `stat`, `list`, or `read` after receiving `fs.changed`. The runtime MUST filter invisible entries before generating, coalescing, or dropping events. It SHOULD batch and rate-limit events to reduce timing disclosure. **`unwatch(watchId)`** — Stops a watch. Unknown `watchId` values MAY be treated as successful no-ops. The runtime MUST scope the identifier to the requesting napplet before lookup. An identifier owned by another napplet MUST be indistinguishable from an unknown identifier. ### Wire Protocol `fs.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `fs.info` | napplet -> runtime | `id` | | `fs.info.result` | runtime -> napplet | `id`, `info?`, `error?` | | `fs.pickFile` | napplet -> runtime | `id`, `options?` | | `fs.pickFile.result` | runtime -> napplet | `id`, `result?`, `error?` | | `fs.pickFiles` | napplet -> runtime | `id`, `options?` | | `fs.pickFiles.result` | runtime -> napplet | `id`, `result?`, `error?` | | `fs.pickDirectory` | napplet -> runtime | `id`, `options?` | | `fs.pickDirectory.result` | runtime -> napplet | `id`, `result?`, `error?` | | `fs.pickSaveFile` | napplet -> runtime | `id`, `options?` | | `fs.pickSaveFile.result` | runtime -> napplet | `id`, `result?`, `error?` | | `fs.stat` | napplet -> runtime | `id`, `path` | | `fs.stat.result` | runtime -> napplet | `id`, `metadata?`, `error?` | | `fs.list` | napplet -> runtime | `id`, `path` | | `fs.list.result` | runtime -> napplet | `id`, `entries?`, `error?` | | `fs.read` | napplet -> runtime | `id`, `path`, `options?` | | `fs.read.result` | runtime -> napplet | `id`, `result?`, `error?` | | `fs.write` | napplet -> runtime | `id`, `path`, `data`, `options?` | | `fs.write.result` | runtime -> napplet | `id`, `result?`, `error?` | | `fs.mkdir` | napplet -> runtime | `id`, `path`, `options?` | | `fs.mkdir.result` | runtime -> napplet | `id`, `error?` | | `fs.remove` | napplet -> runtime | `id`, `path`, `recursive?` | | `fs.remove.result` | runtime -> napplet | `id`, `error?` | | `fs.move` | napplet -> runtime | `id`, `fromPath`, `toPath` | | `fs.move.result` | runtime -> napplet | `id`, `error?` | | `fs.watch` | napplet -> runtime | `id`, `path`, `options?` | | `fs.watch.result` | runtime -> napplet | `id`, `watchId?`, `error?` | | `fs.changed` | runtime -> napplet | `change` | | `fs.unwatch` | napplet -> runtime | `id`, `watchId` | | `fs.unwatch.result` | runtime -> napplet | `id`, `error?` | Key design notes: - Request/result pairs use `id` for correlation. - `fs.changed` is runtime-pushed and has no `id`. - `watchId` is generated by the runtime and scoped to the requesting napplet. - Every request receives one result message with the same `id`. - A successful result MUST omit `error` and include every operation-specific success field shown in the API surface. A failed result MUST include `error` and MUST omit all operation-specific success fields. - A runtime MAY return `not-found` instead of `permission-denied` to avoid revealing hidden paths. It SHOULD normalize error detail and timing for hidden paths. #### Byte Encoding `fs.write.data` and `FsReadResult.data` carry file bytes as base64 text in the JSON wire envelope. - The encoding MUST use the RFC 4648 standard base64 alphabet with padding. - Empty bytes MUST be encoded as the empty string. - Runtimes MUST reject malformed base64, omitted padding, whitespace, base64url, and non-canonical encodings with `invalid-data`. - `bytesRead`, `bytesWritten`, `FsLimits.maxReadBytes`, `FsLimits.maxWriteBytes`, and `FsLimits.maxInFlightBytes` count decoded bytes, not the encoded JSON string length. - Runtimes MAY separately enforce encoded envelope size limits. Those limits are runtime policy and do not change the `FsLimits` byte counts. #### Examples **Info:** ``` -> { "type": "fs.info", "id": "i1" } <- { "type": "fs.info.result", "id": "i1", "info": { "roots": [ { "path": "/shared", "name": "Shared files", "permissions": ["read", "list", "write", "create", "delete", "watch"] }, { "path": "/media", "name": "Media", "permissions": ["read", "list", "watch"] } ], "limits": { "maxReadBytes": 1048576, "maxWriteBytes": 1048576, "maxWatchCount": 16 } } } ``` **Pick text file:** ``` -> { "type": "fs.pickFile", "id": "p1", "options": { "permissions": ["read"], "accept": [{ "mime": "text/plain" }, { "extension": ".md" }] } } <- { "type": "fs.pickFile.result", "id": "p1", "result": { "entries": [{ "path": "/picked/report.md", "kind": "file", "name": "report.md", "permissions": ["read"], "size": 1200 }] } } ``` **Pick directory:** ``` -> { "type": "fs.pickDirectory", "id": "p2", "options": { "permissions": ["read", "list", "watch"], "description": "Choose media folder" } } <- { "type": "fs.pickDirectory.result", "id": "p2", "result": { "entries": [{ "path": "/picked/media", "kind": "directory", "name": "media", "permissions": ["read", "list"] }] } } ``` **Pick save destination:** ``` -> { "type": "fs.pickSaveFile", "id": "p3", "options": { "permissions": ["write", "create"], "suggestedName": "export.json", "accept": [{ "mime": "application/json" }] } } <- { "type": "fs.pickSaveFile.result", "id": "p3", "result": { "entries": [{ "path": "/picked/export.json", "kind": "file", "name": "export.json", "permissions": ["write", "create"] }] } } ``` **Pick cancelled:** ``` -> { "type": "fs.pickFile", "id": "p4" } <- { "type": "fs.pickFile.result", "id": "p4", "error": "cancelled" } ``` **Range read:** ``` -> { "type": "fs.read", "id": "r1", "path": "/shared/video.bin", "options": { "offset": 1048576, "length": 65536 } } <- { "type": "fs.read.result", "id": "r1", "result": { "data": "SGVsbG8gd29ybGQh", "offset": 1048576, "bytesRead": 12, "eof": false, "size": 9000000 } } ``` **Replace file:** ``` -> { "type": "fs.write", "id": "w1", "path": "/shared/note.txt", "data": "SGVsbG8gd29ybGQh", "options": { "mode": "replace" } } <- { "type": "fs.write.result", "id": "w1", "result": { "bytesWritten": 12, "size": 12 } } ``` **Patch file:** ``` -> { "type": "fs.write", "id": "w2", "path": "/shared/db.bin", "data": "AAECAw==", "options": { "mode": "patch", "offset": 4096 } } <- { "type": "fs.write.result", "id": "w2", "result": { "bytesWritten": 4, "size": 8192 } } ``` **Recursive mkdir:** ``` -> { "type": "fs.mkdir", "id": "m1", "path": "/shared/projects/new", "options": { "recursive": true } } <- { "type": "fs.mkdir.result", "id": "m1" } ``` **List directory:** ``` -> { "type": "fs.list", "id": "l1", "path": "/shared" } <- { "type": "fs.list.result", "id": "l1", "entries": [{ "name": "note.txt", "path": "/shared/note.txt", "kind": "file", "size": 12 }] } ``` **Recursive watch:** ``` -> { "type": "fs.watch", "id": "x1", "path": "/shared", "options": { "recursive": true } } <- { "type": "fs.watch.result", "id": "x1", "watchId": "watch-1" } <- { "type": "fs.changed", "change": { "watchId": "watch-1", "path": "/shared/note.txt", "kind": "modified" } } ``` **Permission denied:** ``` -> { "type": "fs.read", "id": "r2", "path": "/hidden/private.txt" } <- { "type": "fs.read.result", "id": "r2", "error": "not-found" } ``` ### Runtime Behavior - The runtime MUST authorize every request against the napplet identity assigned at creation time by the projection. - The runtime MUST respond to every request with a result message carrying the same `id`. - The runtime MUST NOT trust any path, permission, root, watch identifier, picker option, or picker filter supplied by the napplet beyond using it as an untrusted request parameter. - The runtime MUST enforce the napplet's view on `pickFile`, `pickFiles`, `pickDirectory`, `pickSaveFile`, `stat`, `list`, `read`, `write`, `mkdir`, `remove`, `move`, `watch`, and `unwatch`. - The runtime MUST NOT expose host absolute paths or backing-store identifiers. - The runtime MUST mediate picker operations through runtime policy and, when required, user choice. - The runtime MUST expose picked entries only as virtual paths in the napplet's visible filesystem. - The runtime MAY require a user gesture before showing picker UI. - The runtime MUST enforce `FsLimits.maxReadBytes` and `FsLimits.maxWriteBytes` as decoded byte limits. - The runtime SHOULD enforce per-napplet limits on active watches, in-flight requests, aggregate in-flight bytes, recursive work, and operation duration. - The runtime MAY reject work beyond those limits with `too-large`, `quota-exceeded`, or `policy-denied`. - The runtime MAY use any persistence, sync, conflict-resolution, or backing-store strategy as long as the napplet-observed contract is preserved. - The runtime MAY report stale metadata when its backing store is eventually consistent. - The runtime MAY revoke permissions or picked access during a session. Subsequent operations MUST reflect the new policy. ### Security Considerations - Backing storage can be shared across napplets. Shared mutation is destructive authority, not a consequence of visibility. The runtime MUST enforce each napplet's mutable scope and explicit policy for destructive shared operations. - Host paths are sensitive. Exposing them leaks usernames, device layout, mounted volumes, and runtime internals. NAP-FS uses virtual paths only. - Picker results are subject to the same host-path secrecy rules. They MUST NOT reveal host paths, volume names, storage providers, or backing-store details. - Path traversal is a primary attack surface. Runtimes MUST reject `.` and `..`, normalize before authorization, and prevent mount or symlink escape. - Recursive watch can leak hidden descendants if implemented naively. Runtimes MUST derive event delivery only from activity visible to the watching napplet. Event payloads, filtering, coalescing, and drop behavior MUST NOT reveal hidden entries. Runtimes SHOULD batch and rate-limit events to reduce timing channels. - Large reads, writes, recursive operations, and watches are denial-of-service surfaces. Runtimes SHOULD enforce quotas, chunk limits, aggregate byte limits, request limits, watch limits, and operation timeouts. - Picker UI can be abused for prompt spam and focus disruption. Runtimes SHOULD rate-limit picker requests and MAY require user activation. - Picker filters are advisory. Runtimes and napplets MUST NOT infer safe content from MIME types, extensions, suggested names, or picker descriptions. - Picker results reveal user choices and may reveal names, sizes, timestamps, and kinds. Runtimes SHOULD minimize returned metadata and SHOULD avoid denial detail that reveals whether a hidden or host object exists. - `info()` is not an authorization token. Napplets MUST handle operation failures even when `info()` advertised a matching permission. - File and directory names, sizes, timestamps, kinds, and change timing are sensitive even when file bytes are unavailable. Runtimes SHOULD expose only policy-authorized metadata and SHOULD omit or coarsen timestamps and sizes when exact values are unnecessary. - Concurrent writes can conflict. Napplets SHOULD use `revision` and write preconditions when lost updates matter. Without a precondition, the runtime MAY apply runtime-defined ordering. Metadata and watch events can be stale. - An `unknown` entry kind grants no implied operation. The runtime MUST reject `read` or `list` unless it can enforce the corresponding safe file or directory semantics. - File bytes are visible to the runtime and cross the projection boundary. Napplets SHOULD NOT treat NAP-FS as confidential storage from the runtime. ### Implementations - (none yet) ### Changelog - `e63e73b` - Introduced NAP-FS for shell-mediated virtual filesystem access. - `pending` - Hardened shared mutation, concurrency, metadata, and watch policy. - `pending` - Added user-mediated picker operations that expose selected files and directories as virtual paths with runtime-defined persistence. - `pending` - Defined base64 byte encoding, decoded-byte limits, schema tables, and `invalid-data` for malformed byte payloads. - `3ad611a` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #88 ↗](https://github.com/napplet/naps/pull/88)[Read proposed changes ↗](https://github.com/napplet/naps/pull/88/files) On this pageSpecification metadata Capability domain `fs` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `bd1b9e912f05` [View source on GitHub ↗](https://github.com/napplet/naps/pull/88/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-GAMEPAD / PR #130 # Shell-Mediated Gamepad Input Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: supported** Cross-repository delivery includes binding, security, runtime, and conformance verification; the reopened proposal is still subject to change. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/130), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/0b28f77e3dfa0f07dd7c0f9d61a7090ee99f3a5e/naps/NAP-GAMEPAD.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/130/files). ## NAP-GAMEPAD ### Shell-Mediated Gamepad Input `draft` **NAP ID:** NAP-GAMEPAD **Domain:** `gamepad` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.gamepad`; domain presence signals availability. ### Description NAP-GAMEPAD gives napplets game controller input without direct device access. A controller is a user-level device: every napplet that can read it natively sees every press, so napplets running side by side all react to the same input. The runtime instead owns the only native reader. The shell delivers controller snapshots to subscribed napplets and gives live values only to the napplet that has input focus. Other napplets see which controllers are connected, but their values are neutral. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `getGamepads` | none | list of `GamepadSlot` | latest `gamepad.state` | | `available` | none | boolean | latest `gamepad.state` | | `focused` | none | boolean | latest `gamepad.state` | | `onChange` | handler for `GamepadState` | `Subscription` handle | `gamepad.state` | #### Schemas `GamepadSlot` is a `Gamepad` or `null`. `null` marks an empty slot. `GamepadButton` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `value` | yes | number | `0` to `1`. Analog travel for triggers; `0` or `1` for digital buttons. | | `pressed` | yes | boolean | Device-reported pressed state. | | `touched` | yes | boolean | Device-reported touch state; equals `pressed` when the device has no touch sensing. | `Gamepad` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `index` | yes | integer | Slot position. Stable while the controller stays connected. | | `id` | yes | text | Runtime-reported controller description. Not an authenticated identity. | | `mapping` | yes | text | `"standard"` when buttons and axes follow the [W3C standard layout](https://www.w3.org/TR/gamepad/#remapping); `""` otherwise. Other values MAY appear. | | `connected` | yes | boolean | `true` in every slot of a snapshot. | | `timestamp` | yes | number | Runtime time of the last input change, in milliseconds. `0` in neutral slots. | | `axes` | yes | list of number | Each `-1` to `1`. | | `buttons` | yes | list of `GamepadButton` | Button order follows `mapping`. | `GamepadUnavailableReason` accepts `"blocked"` or `"unavailable"`. `GamepadState` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `available` | yes | boolean | `false` when the runtime cannot read controllers. | | `reason` | no | `GamepadUnavailableReason` | Present only when `available` is `false`. `"blocked"` means host policy denied the runtime; `"unavailable"` means no controller API exists. | | `focused` | yes | boolean | `true` when this napplet currently receives live values. | | `pads` | yes | list of `GamepadSlot` | Complete slot list in runtime order. Not a diff. | **`getGamepads()`** returns the slots from the latest snapshot. Before the first snapshot arrives it returns only `null` slots, possibly none. It never waits for the wire. **`available`** and **`focused`** return those fields from the latest snapshot. Before the first snapshot, `available` is `true` and `focused` is `false`. **`onChange(handler)`** calls `handler` with each new `GamepadState` and returns a `Subscription` handle. Closing the handle removes the local handler. The runtime-provided binding sends `gamepad.subscribe` before or on first use. It MAY subscribe when the domain is installed. It MAY send `gamepad.unsubscribe` when the napplet no longer needs input. ### Wire Protocol `gamepad.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `gamepad.subscribe` | napplet -> shell | none | | `gamepad.unsubscribe` | napplet -> shell | none | | `gamepad.state` | shell -> napplet | `available`, `reason`?, `focused`, `pads` | Key design notes: - All three messages are fire-and-forget. None carries `id`. - `gamepad.state` is a complete snapshot, sent right after `gamepad.subscribe` and then whenever the napplet's view changes. - Snapshots are pushed, not polled. A napplet reads the latest snapshot locally as often as it renders. #### Examples **Subscribe; this napplet has focus:** ``` -> { "type": "gamepad.subscribe" } <- { "type": "gamepad.state", "available": true, "focused": true, "pads": [ null, { "index": 1, "id": "Xbox Wireless Controller", "mapping": "standard", "connected": true, "timestamp": 18235.4, "axes": [0.0, -1.0, 0.0, 0.0], "buttons": [{ "value": 1, "pressed": true, "touched": true }, { "value": 0, "pressed": false, "touched": false }] } ]} ``` **Focus moves to another napplet; this napplet receives a neutral snapshot:** ``` <- { "type": "gamepad.state", "available": true, "focused": false, "pads": [ null, { "index": 1, "id": "Xbox Wireless Controller", "mapping": "standard", "connected": true, "timestamp": 0, "axes": [0, 0, 0, 0], "buttons": [{ "value": 0, "pressed": false, "touched": false }, { "value": 0, "pressed": false, "touched": false }] } ]} ``` **Host policy denies controller access to the runtime:** ``` <- { "type": "gamepad.state", "available": false, "reason": "blocked", "focused": false, "pads": [] } ``` **Stop receiving input:** ``` -> { "type": "gamepad.unsubscribe" } ``` #### Error Handling There are no request/result pairs. Unavailability is reported in state through `available` and `reason`. The shell silently ignores malformed or excessive subscription messages. ### Shell Behavior - The runtime MUST be the only reader of physical controllers on behalf of napplets. Where the host offers a native controller API, the runtime MUST deny it to napplets. - The shell MUST send a `gamepad.state` snapshot after each `gamepad.subscribe`. It MUST then send a snapshot whenever that napplet's view changes, and SHOULD NOT resend an unchanged snapshot. - The shell SHOULD send at most one snapshot per display frame per napplet. - The shell MUST deliver live `axes`, `buttons` and `timestamp` values only to the napplet that holds input focus, and only while the host surface is visible and focused. - For every other subscribed napplet, the shell MUST send each connected slot with every axis `0`, every button `value` `0` with `pressed` and `touched` `false`, and `timestamp` `0`. It MUST send this neutral snapshot as soon as focus leaves a napplet. - The shell MUST report connection changes to every subscribed napplet, including napplets without focus. - The shell MUST report `available: false` with a `reason` when the runtime itself cannot read controllers. - The shell MUST stop sending `gamepad.state` after `gamepad.unsubscribe` and when the napplet's session ends. - The shell SHOULD clamp axes to `-1`..`1` and button values to `0`..`1`. It MAY cap the number of slots, axes and buttons, and SHOULD rate-limit subscription changes. - The shell MAY enforce ACL checks on `gamepad` capabilities. ### Napplet Guidance - A napplet SHOULD treat `focused: false` as idle input, not as release events to act on. - A button that is already held when `focused` becomes `true` SHOULD NOT be treated as a new press. - A napplet SHOULD route players by `index`, not by list position, and SHOULD keep keyboard and pointer controls. - The domain is optional. A napplet SHOULD NOT declare `gamepad` as required unless it cannot run without controllers. ### Web Projection Guidance This section is projection guidance only. The contract above does not change. - The shell denies the native API to napplet frames with the Permissions-Policy feature `gamepad`, for example `allow="gamepad 'none'"` on each napplet `iframe`. CSP cannot govern controller access. - Denying the feature leaves `navigator.getGamepads` present in the frame, but calls to it throw `SecurityError`. A binding MAY then replace only that operation with one served from `gamepad.state`. Existing games and libraries then keep working unchanged. Such a replacement SHOULD: - keep the native property attributes, name, length and receiver check; - return the host's native slot shape, including `null` empty slots; - give pads and buttons the native `Gamepad` and `GamepadButton` prototypes, and dispatch `gamepadconnected` / `gamepaddisconnected` with `GamepadEvent`'s prototype; - throw `SecurityError` when the shell reports `"blocked"`; and - leave the native API untouched when it is not denied. ### Security Considerations - Controller input is user input. The isolation guarantee depends on the runtime denying every native controller path to napplets. A shell that leaves the native API reachable provides no isolation. - Neutral snapshots carry no values or timing, so a napplet without focus cannot observe another napplet's input. - `id` strings can identify controller models and aid fingerprinting. The shell MAY generalize them. Napplets MUST NOT treat `id` as an authenticated or stable device identity. - Subscriptions cost the runtime a polling loop. The shell SHOULD bound subscription churn and SHOULD stop polling when no napplet is subscribed. - This domain grants no device commands. Haptics, raw HID access and remapping are out of scope. ### Implementations - [napplet.soy](https://github.com/hzrd149/napplet-soy/tree/feat/gamepad-isolation): shell broker ([`gamepad-session.ts`](https://github.com/hzrd149/napplet-soy/blob/feat/gamepad-isolation/packages/runtime/src/gamepad-session.ts)), web binding with native-API replacement ([`prelude.ts`](https://github.com/hzrd149/napplet-soy/blob/feat/gamepad-isolation/packages/runtime/src/prelude.ts)), and Permissions-Policy denial. ### Changelog - `31b3613` - Introduced NAP-GAMEPAD for focus-scoped, shell-mediated controller snapshots. [Join the discussion · PR #130 ↗](https://github.com/napplet/naps/pull/130)[Read proposed changes ↗](https://github.com/napplet/naps/pull/130/files) On this pageSpecification metadata Capability domain `gamepad` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `0b28f77e3dfa` [View source on GitHub ↗](https://github.com/napplet/naps/pull/130/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-GIT / PR #137 # Runtime-Mediated Git Repositories Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/a73bf8de19db9061a9f40503c3c12c8ff4c261a1/naps/NAP-GIT.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/137/files). ## NAP-GIT ### Runtime-Mediated Git Repositories `draft` **NAP ID:** NAP-GIT **Domain:** `git` **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.git`; domain presence signals availability. This is projection guidance. ### Description NAP-GIT provides repository access through Git objects, references, and explicit remote operations. The napplet designs browsing and editing workflows. The shell mediates repository selection and authority. The runtime executes Git operations, holds credentials, signs when required, and selects storage and network backends. The same contract supports native Git libraries or executables and [ngit](https://github.com/DanConwayDev/ngit-cli), which combines Git object storage with Nostr repository state and collaboration. #### Boundary A Git object is immutable content identified by its hash: a blob (file bytes), tree (directory entries), commit (a tree and its history), or annotated tag. A reference is a mutable name pointing to an object. These concepts are common to both implementations; the runtime's implementation choice is not a projection. | Concern | Owner | | --- | --- | | Repository access, object verification, storage, retention | Runtime | | Local reference transactions, remote synchronization | Runtime | | Credentials, signer selection, Nostr event construction and verification | Runtime, under shell mediation | | Editors, comparisons, history presentation, conflict-resolution choices | Napplet | | Repository, account, publication destinations, permission decisions | User, through the shell | | Host paths, processes, sockets, Git configuration and command execution | Runtime internals | Creating a commit, moving a local reference, updating a remote reference, and submitting a proposal are separate effects. The runtime MUST NOT substitute one for another. In particular, ngit's proposal-producing branch conventions MUST NOT turn `push` into `propose`. This draft exposes committed objects. Working directories, staging indexes, checkout, merge/rebase execution, arbitrary commands, issues, reviews, repository announcement editing, and forge administration are outside this surface. Napplets can compare trees and construct an edited commit without host filesystem access. Filesystem and generic Nostr access remain independently governed by their owning domains; no other NAP domain is required by this contract. ### API Surface Every runtime exposing `git` MUST implement `open`, `close`, `refs`, `get`, and `read`. Additional operations are reported per repository in `operations`. An absent operation MUST return `unsupported` when its defined request is received. An advertised operation can still be denied by current policy. | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `open` | optional `source` (text) | `GitRepository` | `git.open` / `git.open.result` | | `close` | `repository` (text) | `null` | `git.close` / `git.close.result` | | `refs` | `repository` (text), optional `cursor` (text), optional `limit` (integer) | `GitRefPage` | `git.refs` / `git.refs.result` | | `get` | `repository` (text), `oid` (text), optional `cursor` (text), optional `limit` (integer) | `GitObject` | `git.get` / `git.get.result` | | `read` | `repository` (text), `oid` (text), `offset` (integer), `length` (integer) | `GitBytes` | `git.read` / `git.read.result` | | `createCommit` | `repository` (text), `request` (`GitCommitRequest`) | `GitCreatedCommit` | `git.createCommit` / `git.createCommit.result` | | `updateRef` | `repository` (text), `request` (`GitRefUpdate`) | `GitRefUpdate` | `git.updateRef` / `git.updateRef.result` | | `fetch` | `repository` (text), `remote` (text), `ref` (text) | `GitFetchedRef` | `git.fetch` / `git.fetch.result` | | `push` | `repository` (text), `request` (`GitPushRequest`) | `GitPublication` | `git.push` / `git.push.result` | | `propose` | `repository` (text), `request` (`GitProposalRequest`) | `GitPublication` | `git.propose` / `git.propose.result` | #### Common values | Value | Definition | | --- | --- | | Object ID (`oid`) | Full lowercase hexadecimal Git object ID: 40 characters for `sha1`, 64 for `sha256`, matching the opened repository's `objectFormat`. Abbreviations, revision expressions, and all-zero IDs are invalid. A Git ID is distinct from a Nostr event ID. | | Reference name (`ref`) | UTF-8 text containing a full `refs/heads/…` or `refs/tags/…` name accepted by [Git's reference-name rules](https://git-scm.com/docs/git-check-ref-format). No refspecs, wildcards, revision expressions, or symbolic references. Branch mutations require `refs/heads/…`. | | Bytes | Text containing canonical padded standard base64 under [RFC 4648](https://www.rfc-editor.org/rfc/rfc4648#section-4). Empty bytes are `""`. Whitespace, base64url, missing required padding, and nonzero pad bits are invalid. | | Path | Bytes encoding a repository-relative path. `/` byte separates segments. Empty, absolute, trailing-slash, empty-segment, NUL, `.` and `..` paths are invalid. No Unicode normalization, case folding, URL decoding, or host path interpretation. | | Name | Bytes encoding one nonempty tree-entry name; `/` and NUL are invalid. Names need not be UTF-8. | | Cursor | Nonempty opaque text scoped to the endpoint, repository handle, operation, and original arguments. Not authority. | All integers MUST be exact and within -9007199254740991 through 9007199254740991; byte counts and offsets MUST be nonnegative. List limits MUST be positive. Omitted optional fields use the defaults stated below. `null` is valid only where explicitly allowed. Empty lists mean no entries. #### Repository and reference schemas `GitRepository`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `repository` | yes | text | Nonempty opaque handle for this authenticated endpoint. | | `objectFormat` | yes | `"sha1"` or `"sha256"` | Applies to every Git object ID on this handle. | | `operations` | yes | list of text | Supported operation names from the API table. No duplicates. | | `remotes` | yes | list of `GitRemote` | Only remotes the caller may discover. Empty for local-only access. | `GitRemote`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `remote` | yes | text | Nonempty opaque identifier scoped to this repository handle. | | `operations` | yes | list of text | Supported subset of `"fetch"`, `"push"`, `"propose"`; not a grant. | `GitRef`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ref` | yes | text | Full reference name. | | `oid` | yes | text | Direct target; an annotated tag points to its tag object, not its peeled commit. | `GitRefPage`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `entries` | yes | list of `GitRef` | Local references, sorted by unsigned UTF-8 bytes of `ref`. | | `next` | yes | text or `null` | Cursor for the next page; `null` means complete. | `GitRefUpdate`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ref` | yes | text | Local branch reference. | | `expected` | yes | text or `null` | Current direct object ID; `null` requires absence. | | `oid` | yes | text or `null` | New commit ID; `null` deletes. Both `expected` and `oid` cannot be `null`. | `GitFetchedRef`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ref` | yes | text | Requested remote reference. | | `oid` | yes | text | Observed direct target, now available locally. | #### Object schemas `GitObject`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `oid` | yes | text | Requested object ID. | | `kind` | yes | `"blob"`, `"tree"`, `"commit"`, or `"tag"` | Selects the fields below. | | `size` | yes | integer | Complete uncompressed Git object body size in bytes. | | `entries` | conditional | list of `GitTreeEntry` | Required only for a tree; one page of immediate children. | | `next` | conditional | text or `null` | Required only for a tree. | | `commit` | conditional | `GitCommit` | Required only for a commit. | | `target` | conditional | text | Required only for a tag; target object ID. | Fields for other kinds MUST be absent. Blobs contain no byte payload in `get`; use `read`. An annotated tag's content and signature remain available through `read`; `target` does not assert signature validity. `GitTreeEntry`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `name` | yes | text | Name bytes. | | `mode` | yes | `"040000"`, `"100644"`, `"100755"`, `"120000"`, or `"160000"` | Tree, regular blob, executable blob, symlink blob, or gitlink (submodule commit). | | `oid` | yes | text | Child object ID. A gitlink's object can be absent from this repository. | `GitCommit`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `tree` | yes | text | Root tree object ID. | | `parents` | yes | list of text | Parent commit IDs in stored order; empty for a root commit. | | `author` | yes | text | Raw author-header value, encoded as bytes. Attribution, not authenticated identity. | | `committer` | yes | text | Raw committer-header value, encoded as bytes. | | `message` | yes | text | Exact message bytes. No character encoding is assumed. | `GitBytes`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `data` | yes | text | Requested bytes from the uncompressed object body, without Git's object header. | | `offset` | yes | integer | Requested offset. | | `size` | yes | integer | Complete body size. | | `eof` | yes | boolean | True when offset plus decoded data length reaches or exceeds `size`. | `read` works for all four object kinds. Its raw bytes preserve headers, signatures, non-UTF-8 messages, and fields omitted from the structured view. The runtime MUST verify the object's Git hash, including its type/size header, before returning any part of it. Replacement objects and grafted history MUST NOT alter the requested object's view. #### Commit schemas `GitCommitRequest`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `parent` | yes | text or `null` | Parent commit, or `null` for a root commit with an initially empty tree. | | `changes` | yes | list of `GitChange` | Nonempty, distinct paths; applied to the parent's tree. | | `message` | yes | text | Nonempty UTF-8 commit message; NUL is invalid. | `GitChange`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `path` | yes | text | Path bytes. | | `action` | yes | `"put"` or `"delete"` | Delete removes one existing non-tree entry. | | `mode` | conditional | `"100644"` or `"100755"` | Required only for `put`. | | `data` | conditional | text | File bytes; required only for `put`, including empty files. | `GitCreatedCommit`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `oid` | yes | text | Created commit object ID. | #### Publication schemas `GitPushRequest`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `remote` | yes | text | Remote identifier from `GitRepository.remotes`. | | `ref` | yes | text | Exact destination branch. | | `oid` | yes | text | Locally available commit to publish. | `GitProposalRequest`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `remote` | yes | text | Remote identifier supporting `propose`. | | `target` | yes | text | Intended destination branch. | | `base` | yes | text | Locally available commit; ancestor of `head`. | | `head` | yes | text | Locally available commit, distinct from `base`. | | `title` | yes | text | Nonempty plain text. | | `description` | yes | text | Plain text; may be empty. | `GitPublication`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `outcome` | yes | `"complete"`, `"failed"`, `"partial"`, or `"unknown"` | Derived from receipts under the rules below. | | `receipts` | yes | list of `GitReceipt` | Nonempty; every required publication effect is represented. | `GitReceipt`: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `effect` | yes | `"objects"`, `"ref"`, or `"proposal"` | Object availability, reference publication, or proposal publication. | | `destination` | yes | text | Nonempty opaque destination identifier, stable within this repository handle. | | `status` | yes | `"accepted"`, `"rejected"`, or `"unknown"` | Positive acknowledgement, known no effect at this destination, or uncertain effect. | | `oid` | yes | text | Requested push commit or proposal head. | | `ref` | conditional | text | Required only for `ref` effects; exact destination branch. | | `locator` | no | text | Credential-free absolute URI identifying the published artifact, when available. Informational; conveys no authority. | ### Operation Semantics #### Repository access and lifetime `open` without `source` asks the shell to let the user choose a repository or create an empty runtime-managed repository. With `source`, it requests access to that repository location. The shell MAY offer a choice, require consent, use an existing authorized store, or deny the request. Local host paths and `file:` URLs are invalid. Supported remote URL schemes are runtime policy; unsupported ones return `unsupported`. `nostr:` locations use the upstream [NIP-34 clone URL format](https://github.com/nostr-protocol/nips/blob/master/34.md#nostr-clone-url-format). Success means the runtime established repository access and can service core reads. It does not promise that every remote object is cached. `open` MAY fetch under shell policy, but MUST NOT publish or alter an existing repository's refs. The runtime MUST NOT equate matching URLs, root commits, or object hashes with identical repository authority. The returned remote identifiers bind the approved logical repository and its authority, not an arbitrary Git server named by data. Handles, remotes, and cursors MUST be scoped to the authenticated endpoint. Copying a handle to another endpoint grants nothing. Policy MAY revoke access. Unknown, foreign, closed, or revoked handles return `invalid-handle` without revealing whether another endpoint owns them. `close` prevents new operations on the handle and waits for already-started operations to settle before returning `null`. Repeated close on the caller's already-closed handle succeeds. Close releases access and cursors; it does not delete repositories, undo commits, or retract publications. Endpoint teardown invalidates all handles and cursors. The runtime MUST stop work that has not crossed its effect boundary and MUST NOT start further publication steps under the revoked endpoint. Already-issued effects may finish; the runtime MUST retain their outcome for shell inspection and MUST NOT send late results to another endpoint. Policy revocation follows the same effect rule. Objects read, created, or fetched through a handle MUST remain available for that handle's lifetime unless access is revoked or storage fails. References may be shared with other authorized callers; the handle is not a private clone. #### Reads and pagination `refs` lists authorized local branch and tag references. It performs no remote refresh. Remote state is obtained with `fetch`. A first page captures one local reference snapshot; subsequent pages MUST continue that snapshot, even if refs move. An empty repository returns `entries: []`, `next: null`. The runtime MUST NOT normalize or lossily decode reference names. A visible reference whose name cannot be represented as UTF-8 makes listing fail with `unsupported` rather than silently hiding that reference. `get` returns a verified object's structured view. Tree entries are sorted lexicographically by unsigned name bytes. `cursor` and `limit` are valid only for trees. For both paginated operations, omission of `limit` selects a runtime default. The runtime MAY cap the page size but MUST use `next` rather than silently truncate. Continuations MUST repeat the original arguments, including the same limit or its omission. Invalid or expired cursors return `invalid-cursor`; the caller restarts from the first page. An incomplete page MUST advance. `read` returns exactly `min(length, max(0, size - offset))` bytes. An offset at or beyond the end returns empty bytes and `eof: true`. Zero length is valid. The runtime MUST reject an excessive requested length with `too-large`, rather than shorten a successful read. It MAY obtain authorized missing objects from approved backends; inability to obtain an object returns `not-found` or `unavailable`. Symlinks and gitlinks are data; the runtime MUST NOT follow them to host files or submodule repositories. #### Local mutations `createCommit` constructs blobs, trees, and one commit. Parent ancestry MUST be preserved exactly. It does not update a reference, staging index, working directory, or remote. The shell selects the author, committer, timestamps, and any Git signing identity under user policy. No napplet field selects a signer. The returned object's stored headers are inspectable with `get` and `read`. The message MUST be stored as its exact UTF-8 bytes, without normalization or line-ending changes. Git signatures and Nostr event signatures are separate mechanisms. The runtime MUST validate all changes before constructing the commit. Changes MUST NOT overlap as ancestor/descendant paths. `put` replaces or creates only a regular file; replacing a tree, symlink, or gitlink returns `conflict`. Missing parent directories are created. Traversing a non-tree returns `conflict`. Deleting a missing entry returns `not-found`; deleting the last entry removes empty ancestor trees. Unchanged entries and their modes MUST be preserved. The runtime MUST reject `.git` path segments, ASCII case-insensitively, and backslash bytes in changes. These restrictions do not hide existing tree names from reads. Invalid input or failure MUST NOT update refs; unreachable objects left by failed creation MAY be garbage-collected. `updateRef` is a local compare-and-swap: the runtime MUST atomically compare the current direct target with `expected` and apply `oid`, or return `conflict` without changing that reference. It MUST validate a non-null target as a local commit. Symbolic destinations MUST be rejected. Rewrites and deletions require explicit shell policy. The runtime MUST NOT change a branch attached to a working directory; it MUST return `conflict`. Success returns the applied request record. Runtime-managed bare repositories avoid this restriction. This transaction makes no statement about remote state. #### Remote effects `fetch` resolves one named remote reference according to that remote's authority, obtains its target and reachable Git objects, and returns the observed target. Success requires complete reachable history and trees, excluding gitlink targets. A runtime limited to incomplete history returns `unavailable`. A missing ref returns `not-found`. Fetch MUST NOT move local branches, tags, or working directories. Internal caches and tracking metadata MAY change. A concurrent remote change does not invalidate the returned observation. `push` requests publication of exactly `oid` to the destination branch, with its reachable objects. Only branch creation and fast-forward updates are in this draft. A fast-forward retains the previous commit as an ancestor of the new commit. The runtime MUST check the authorized remote state before publication and apply any stronger checks the backend supports. A known non-fast-forward returns `conflict`. Force, deletion, mirror refspecs, and automatic merge are excluded. A backend that cannot perform the requested reference update MUST return `unsupported`; it MUST NOT silently submit a proposal. Remote publication is not the local `updateRef` transaction. Neither a prior `fetch` nor a successful `push` guarantees exclusive ownership or global compare-and-swap on a distributed remote. The runtime MUST NOT claim such a guarantee. It MUST preserve the exact requested ref and object ID when signing or sending a publication. Competing signed state can later supersede it. When a backend represents several refs in one state document, the runtime MUST preserve unrelated refs from the resolved state. This preserves the observed state; it cannot prevent an unseen concurrent update from being superseded. `propose` submits the commits after `base` through `head` for the named target branch, with the supplied title and description. The runtime MUST verify ancestry and preserve that range. It MUST NOT merge or update the target branch. The runtime owns representation as patches, a remote branch plus pull request, or Nostr proposal events. If it cannot preserve the requested range or metadata, it returns `unsupported`. Native Git alone need not support proposals. This operation creates a new proposal; revisions and lifecycle changes are outside this draft. A proposal receipt SHOULD supply its address as `locator`. Before `push` or `propose`, the shell MUST authorize the complete publication plan: destinations, disclosed history, signer/account, and each required effect. The runtime MUST NOT add destinations after authorization without renewed mediation. Supporting object uploads count as publication even if a later reference or proposal step fails. For patches carrying all required content, a separate object-upload effect is unnecessary. After publication begins, the runtime MUST return `GitPublication`, including on timeout or partial failure, rather than hide receipts in an ordinary error. Every required effect/destination pair MUST have one receipt. Unattempted steps are `rejected`; acceptance requires positive acknowledgement or verified preexisting satisfaction. Outcome is derived in this order: | Condition | `outcome` | | --- | --- | | Any receipt is `unknown` | `"unknown"` | | All receipts are `accepted` | `"complete"` | | All receipts are `rejected` | `"failed"` | | Otherwise | `"partial"` | An accepted `objects` receipt means the required objects are available at that destination. An accepted `ref` receipt means the selected ref authority accepted the exact update; for signed distributed state, selected relay acceptance is the evidence. An accepted `proposal` receipt means the proposal was accepted by its selected service or relay. `push` requires at least one `ref` receipt and no `proposal` receipts. `propose` requires at least one `proposal` receipt and MUST NOT include a `ref` receipt for the target branch. Supporting proposal branches MAY have their own `ref` receipts. `complete` does not promise global consensus, permanent availability, replication to unselected recipients, or merge. ### Native Git and ngit Mapping This table is implementation guidance, not a claim of NAP support. Upstream formats remain owned by [Git](https://git-scm.com/docs) and [NIP-34](https://github.com/nostr-protocol/nips/blob/master/34.md). | Contract | Native Git implementation | ngit implementation | | --- | --- | --- | | `open` | Approved local object store or managed clone | Resolve approved Nostr repository identity; manage a Git object store | | `get`, `read`, `createCommit` | Git object database | The same Git object model; Nostr event IDs never replace Git IDs | | `refs`, `updateRef` | Local refs and atomic local transactions | Local refs and the same local transactions; no implicit event publication | | `fetch` | Resolve remote refs and fetch objects | Resolve authorized signed state and obtain objects from approved Git servers | | `push` | Publish objects and update exact remote branch | Publish objects where needed and authorized signed branch state | | `propose` | Optional forge or patch-delivery integration | Construct and publish the appropriate Nostr proposal representation | ngit separates signed repository state from Git object hosting, as described in its [architecture guide](https://ngit.dev/how-it-works). The runtime MUST verify signatures and the selected repository's maintainer authority before using signed remote state. An announcement or server's possession of objects alone does not authorize its refs. The runtime MUST follow its selected backend's state-selection rules; unresolved conflicting authority returns `conflict`. It MUST NOT substitute a reachable object's host as the state authority when signed state is unavailable. Before publishing signed branch state, the runtime MUST verify that the selected signer has maintainer authority for the opened repository. It MUST return `denied` before publication if that authority is absent. ngit's [proposal branch behavior](https://ngit.dev/pull-requests) belongs behind `propose`. An implementation may use a remote helper internally, but MUST prevent helper naming rules, implicit tracking configuration, or automatic fallbacks from changing the requested effect. Separate receipts expose Git server uploads and relay publication; one successful step does not establish the other. No generic relay or signer surface is exposed to the napplet by `git`. ### Wire Protocol The following language-neutral messages carry this contract. The [web projection](https://github.com/napplet/naps/blob/a73bf8de19db9061a9f40503c3c12c8ff4c261a1/projections/web.md) maps them to the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) envelope. `?` marks optional fields; it is not part of a field name. | Type | Direction | Payload fields | | --- | --- | --- | | `git.open` | napplet -> runtime | `id`, `source?` | | `git.open.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.close` | napplet -> runtime | `id`, `repository` | | `git.close.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.refs` | napplet -> runtime | `id`, `repository`, `cursor?`, `limit?` | | `git.refs.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.get` | napplet -> runtime | `id`, `repository`, `oid`, `cursor?`, `limit?` | | `git.get.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.read` | napplet -> runtime | `id`, `repository`, `oid`, `offset`, `length` | | `git.read.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.createCommit` | napplet -> runtime | `id`, `repository`, `request` | | `git.createCommit.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.updateRef` | napplet -> runtime | `id`, `repository`, `request` | | `git.updateRef.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.fetch` | napplet -> runtime | `id`, `repository`, `remote`, `ref` | | `git.fetch.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.push` | napplet -> runtime | `id`, `repository`, `request` | | `git.push.result` | runtime -> napplet | `id`, `result?`, `error?` | | `git.propose` | napplet -> runtime | `id`, `repository`, `request` | | `git.propose.result` | runtime -> napplet | `id`, `result?`, `error?` | `id` is nonempty text, unique among in-flight requests from that endpoint. The runtime MUST return one corresponding result with the same `id` for every recognized request with a valid `id` from a live authenticated endpoint. Results MUST be routed only to that endpoint. There are no unsolicited messages or wire subscriptions in this draft. Unrecognized message types and messages from unauthenticated endpoints follow the projection's rules. A successful envelope MUST contain `result` of the operation's declared type and omit `error`. A failed envelope MUST contain `error` and omit `result`. A publication result can report an unsuccessful effect through `outcome`; successful delivery of the result does not establish successful publication. Unknown request fields MUST be rejected with `invalid-request` before effects, so misspelled authority or precondition fields cannot silently disappear. Receivers MAY ignore unknown result fields. #### Error Handling `GitError` (`error`): | Field | Required | Type | Notes | | --- | --- | --- | --- | | `code` | yes | text | One value from the table below. | | `message` | no | text | Safe human-readable detail. Not for programmatic branching. | | Code | Meaning | | --- | --- | | `invalid-request` | Missing, malformed, unexpected, or contradictory fields; invalid path, ref, object ID, base64, or range. | | `invalid-handle` | Repository access is absent or ended. | | `invalid-cursor` | Cursor is absent from its scope, expired, or used with different arguments. | | `not-found` | Authorized object, ref, remote, or entry is absent or hidden. | | `denied` | Shell policy or user choice refused the request. | | `cancelled` | User cancelled selection, or work stopped before its observable mutation/publication. | | `unsupported` | Operation, source scheme, object format, or requested semantics cannot be provided. | | `conflict` | Local precondition failed, ancestry is unsuitable, paths conflict, or remote authority cannot be resolved. | | `too-large` | Request, object, or requested read exceeds runtime limits. | | `busy` | Runtime work or storage quota prevents the operation. | | `unavailable` | Required data, transport, credentials, or signer is unavailable. | | `integrity` | Object hash, structure, signature, or authority verification failed. | The runtime MUST validate and authorize before observable mutations or publication. Policy MAY return `not-found` to hide unauthorized data. Ordinary errors establish that the requested mutation/publication did not occur; internal cache fills and unreachable objects are permitted. Started publication follows the receipt rules instead. Invalid or missing `id` cannot be correlated and MAY be discarded. A caller MUST NOT reuse an in-flight ID; a runtime detecting a duplicate MUST NOT execute a second effect or emit a second result for that ID. Correlation IDs are not persistent deduplication keys. After losing a result, the caller MUST treat mutation outcome as uncertain. It SHOULD inspect local refs or remote state and use shell-mediated review before retrying publication. Repeating `propose` can create duplicate public proposals. There is no automatic retry guarantee, reattachment, or exactly-once remote publication in this draft. #### Examples The SHA-1 fixture changes `README.md` from `Hi` plus LF to `Hello` plus LF. The root message is `Initial` plus LF. Both commits use author and committer `Example 0 +0000`, with no extra headers. These are test objects, not a live repository. Encoded paths and bytes are literal base64. **Open, then capture local refs:** ```text -> { "type": "git.open", "id": "o1", "source": "https://git.example.org/tools.git" } <- { "type": "git.open.result", "id": "o1", "result": { "repository": "repo-1", "objectFormat": "sha1", "operations": ["open", "close", "refs", "get", "read", "createCommit", "updateRef", "fetch", "push"], "remotes": [{ "remote": "remote-1", "operations": ["fetch", "push"] }] } } -> { "type": "git.refs", "id": "r1", "repository": "repo-1" } <- { "type": "git.refs.result", "id": "r1", "result": { "entries": [{ "ref": "refs/heads/main", "oid": "eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4" }], "next": null } } ``` **Create an edited commit, then install it with a local precondition:** ```text -> { "type": "git.createCommit", "id": "c1", "repository": "repo-1", "request": { "parent": "eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4", "changes": [{ "path": "UkVBRE1FLm1k", "action": "put", "mode": "100644", "data": "SGVsbG8K" }], "message": "Update README\n" } } <- { "type": "git.createCommit.result", "id": "c1", "result": { "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c" } } -> { "type": "git.updateRef", "id": "u1", "repository": "repo-1", "request": { "ref": "refs/heads/main", "expected": "eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c" } } <- { "type": "git.updateRef.result", "id": "u1", "result": { "ref": "refs/heads/main", "expected": "eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c" } } ``` **Stale local precondition; reference unchanged by this request:** ```text -> { "type": "git.updateRef", "id": "u2", "repository": "repo-1", "request": { "ref": "refs/heads/main", "expected": "eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c" } } <- { "type": "git.updateRef.result", "id": "u2", "error": { "code": "conflict" } } ``` **Inspect the committed tree and read a blob:** ```text -> {"type": "git.get", "id": "g1", "repository": "repo-1", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c"} <- {"type": "git.get.result", "id": "g1", "result": {"oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c", "kind": "commit", "size": 202, "commit": {"tree": "dc0a29da1d9b3f68dcd56af0e34f8df4fbf8b24f", "parents": ["eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4"], "author": "RXhhbXBsZSA8ZXhhbXBsZUBleGFtcGxlLm9yZz4gMCArMDAwMA==", "committer": "RXhhbXBsZSA8ZXhhbXBsZUBleGFtcGxlLm9yZz4gMCArMDAwMA==", "message": "VXBkYXRlIFJFQURNRQo="}}} -> {"type": "git.get", "id": "g2", "repository": "repo-1", "oid": "dc0a29da1d9b3f68dcd56af0e34f8df4fbf8b24f"} <- {"type": "git.get.result", "id": "g2", "result": {"oid": "dc0a29da1d9b3f68dcd56af0e34f8df4fbf8b24f", "kind": "tree", "size": 37, "entries": [{"name": "UkVBRE1FLm1k", "mode": "100644", "oid": "e965047ad7c57865823c7d992b1d046ea66edf78"}], "next": null}} -> {"type": "git.read", "id": "b1", "repository": "repo-1", "oid": "e965047ad7c57865823c7d992b1d046ea66edf78", "offset": 0, "length": 6} <- {"type": "git.read.result", "id": "b1", "result": {"data": "SGVsbG8K", "offset": 0, "size": 6, "eof": true}} ``` **Fetch records an observation without moving the local branch:** ```text -> {"type": "git.fetch", "id": "f1", "repository": "repo-1", "remote": "remote-1", "ref": "refs/heads/main"} <- {"type": "git.fetch.result", "id": "f1", "result": {"ref": "refs/heads/main", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c"}} ``` **ngit publication: object upload accepted, signed-state acceptance uncertain.** Assume `repo-n` and `remote-n` were opened and authorized, and the source commit is a fast-forward of the observed destination. ```text -> { "type": "git.push", "id": "p1", "repository": "repo-n", "request": { "remote": "remote-n", "ref": "refs/heads/main", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c" } } <- {"type": "git.push.result", "id": "p1", "result": {"outcome": "unknown", "receipts": [{"effect": "objects", "destination": "object-store-1", "status": "accepted", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c"}, {"effect": "ref", "destination": "state-relay-1", "status": "unknown", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c", "ref": "refs/heads/main"}]}} ``` **Proposal submission is independent of branch update.** Assume this remote supports `propose`, and both commits are present with the stated ancestry. ```text -> { "type": "git.propose", "id": "p2", "repository": "repo-n", "request": { "remote": "remote-n", "target": "refs/heads/main", "base": "eb7f6a0e5a12e427bec3f9af5fd505fc2efc1fa4", "head": "800cea77c0e8f383c0c6fb6c52b16980d634c17c", "title": "Update README", "description": "Clarify the greeting." } } <- {"type": "git.propose.result", "id": "p2", "result": {"outcome": "complete", "receipts": [{"effect": "proposal", "destination": "proposal-relay-1", "status": "accepted", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c"}]}} ``` **Malformed input and denial despite declared domain availability:** ```text -> { "type": "git.read", "id": "e1", "repository": "repo-1", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c", "offset": -1, "length": 20 } <- { "type": "git.read.result", "id": "e1", "error": { "code": "invalid-request" } } -> { "type": "git.push", "id": "e2", "repository": "repo-1", "request": { "remote": "remote-1", "ref": "refs/heads/main", "oid": "800cea77c0e8f383c0c6fb6c52b16980d634c17c" } } <- { "type": "git.push.result", "id": "e2", "error": { "code": "denied" } } ``` **Release access; subsequent reads fail:** ```text -> {"type": "git.close", "id": "x1", "repository": "repo-1"} <- {"type": "git.close.result", "id": "x1", "result": null} -> {"type": "git.read", "id": "x2", "repository": "repo-1", "oid": "e965047ad7c57865823c7d992b1d046ea66edf78", "offset": 0, "length": 6} <- {"type": "git.read.result", "id": "x2", "error": {"code": "invalid-handle"}} ``` A second endpoint using `repo-1` also receives `invalid-handle`, even while the first endpoint's handle is live. Equal request IDs on different endpoints are independent; neither receives the other's result. If the first endpoint ends while publication is in flight, the shell retains receipts and delivers no late result to the second endpoint. ### Shell Behavior - The shell MUST bind access to the projection-authenticated endpoint and its verified manifest/artifact identity. `git` declarations and handles do not establish user identity or publication permission. - The runtime MUST enforce repository, object/history, reference, remote, and operation scope on every request. Possession of an object ID is not read authority. A policy unable to authorize disclosed history MUST deny access. The runtime MUST deny an object read rather than redact or rewrite the content or structured fields of a hash-identified object. - The shell MUST keep account and repository selection under user control. A source URL, Nostr pubkey, author header, or remote identifier MUST NOT select credentials or grant maintainer authority by itself. - The runtime MUST enforce finite work, byte, pagination, storage, and operation duration limits. It MAY choose their values. Overload MUST produce `busy` or `too-large`; publication deadlines MUST preserve receipts and uncertainty. - The runtime MAY implement this contract using executables, libraries, local stores, or services. It MUST preserve the same observable semantics. ### Security Considerations - Git content and configuration are untrusted. The runtime MUST prevent repository-controlled hooks, filters, credential helpers, diff drivers, Git aliases, command substitutions, and arbitrary remote helpers from executing. Only runtime-approved integrations may run. Napplet fields MUST NOT become command options, environment settings, executable names, or shell code. - The runtime MUST contain object parsing and storage independently of host path interpretation. It MUST bound decompression and graph traversal, reject malformed objects, and prevent traversal through aliases, symlinks, alternates, submodules, or malicious repository configuration into unauthorized stores. - Network destinations from source URLs, redirects, announcements, server responses, or Git metadata are untrusted hints. The runtime MUST apply network policy to every connection and redirect, including discovered object hosts and relays. It MUST NOT send credentials to an unapproved destination. - Publication can disclose entire reachable history, including deleted secrets. The shell MUST authorize that history, not merely the currently edited files. Permission to read or create local commits does not grant upload permission. - Repository handles, remote metadata, receipts, locators, and errors MUST NOT expose host paths, credentials, private keys, signer tokens, or another endpoint's repositories. Private repository names and destinations require explicit disclosure policy. The shell MUST retain signing authority. - Object hashes prove content identity, not author identity, maintainer authority, code safety, or reproducibility of a napplet artifact. Signed repository state also does not make referenced code safe to execute. ### Implementations No implementation of this NAP is verified. Git and ngit are implementation targets; their existing interfaces do not establish support for this draft. ### Changelog - `bb71090` - Defined shared Git object access, commit creation, local reference transactions, remote fetch/push, optional proposals, publication receipts, and native/ngit authority boundaries. [Join the discussion · PR #137 ↗](https://github.com/napplet/naps/pull/137)[Read proposed changes ↗](https://github.com/napplet/naps/pull/137/files) On this pageSpecification metadata Capability domain `git` Repository state Open PR Registry status Not listed in the registry Document marker draft Declared dependencies None declared in the preamble Source revision `a73bf8de19db` [View source on GitHub ↗](https://github.com/napplet/naps/pull/137/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-HASHTREE / PR #73 # Runtime-Mediated Hashtree Document Trees Draft PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/67997706ac6208761dfd1b1d02fe8f13713d7f32/naps/NAP-HASHTREE.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/73/files). ## NAP-HASHTREE ### Runtime-Mediated Hashtree Document Trees `draft` **NAP ID:** NAP-HASHTREE **Domain:** `hashtree` **Depends:** - `identity` -- capability · required -- publishing mutable Hashtree roots uses the current shell-user identity as event author. - `relay` -- capability · required -- resolves and publishes Hashtree root events through the runtime relay surface. - `relay` -- wire · required -- imports `RelayEventResult` for resolved mutable-root event returns. - `resource` -- wire · optional -- imported `RelayEventResult.sidecar.resources?` carries `ResourceSidecarEntry[]`, a type owned by the `resource` domain. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.hashtree`; domain presence signals availability. ### Description NAP-HASHTREE gives napplets a runtime-mediated interface for [Hashtree](https://github.com/mmalmi/hashtree) document trees. A napplet can store bytes, create directories, resolve immutable `nhash` CIDs, resolve mutable `htree:///` roots, read files, list directories, publish root events, and manage pins/import/export jobs. Hashtree's upstream protocol is implemented and has a draft spec (`HTS-01`). This NAP treats HTS-01 as the source for Hashtree bytes: SHA-256 content addresses, deterministic MessagePack tree nodes, CHK encryption, `nhash` identifiers, Nostr kind `30064` mutable roots, and `htree://` URLs. The NAP defines the napplet-facing runtime API around those concepts. The runtime owns storage backends, Blossom/FIPS/local-daemon routing, CHK key handling, root-event signing, relay selection, pin retention, filesystem pickers, and user consent. Napplets never receive direct Blossom, FIPS, filesystem, or signing access through this interface. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `capabilities` | none | `HashtreeCapabilities` | `hashtree.capabilities` / `.result` | | `parseRef` | `ref` (`tstr`) | `HashtreeRefResult` | `hashtree.parseRef` / `.result` | | `resolve` | `ref` (`tstr`), optional `options` (`HashtreeResolveOptions`) | `HashtreeResolveResult` | `hashtree.resolve` / `.result` | | `stat` | `ref` (`tstr`) | `HashtreeEntryResult` | `hashtree.stat` / `.result` | | `list` | `ref` (`tstr`), optional `options` (`HashtreeListOptions`) | `HashtreeListResult` | `hashtree.list` / `.result` | | `read` | `ref` (`tstr`), optional `options` (`HashtreeReadOptions`) | `HashtreeReadResult` | `hashtree.read` / `.result` | | `putBytes` | `request` (`HashtreePutBytesRequest`) | `HashtreePutResult` | `hashtree.putBytes` / `.result` | | `putTree` | `request` (`HashtreePutTreeRequest`) | `HashtreePutResult` | `hashtree.putTree` / `.result` | | `publishRoot` | `request` (`HashtreePublishRootRequest`) | `HashtreePublishRootResult` | `hashtree.publishRoot` / `.result` | | `pickFiles` | optional `options` (`HashtreePickOptions`) | `HashtreeFileSetResult` | `hashtree.pickFiles` / `.result` | | `importFiles` | `request` (`HashtreeImportRequest`) | `HashtreeJobAck` | `hashtree.importFiles` / `.result`, then push messages | | `exportFiles` | `request` (`HashtreeExportRequest`) | `HashtreeJobAck` | `hashtree.exportFiles` / `.result`, then push messages | | `pins` | none | list of `HashtreePin` | `hashtree.pins` / `.result` | | `pin` | `request` (`HashtreePinRequest`) | `HashtreePinResult` | `hashtree.pin` / `.result` | | `unpin` | `ref` (`tstr`) | `HashtreePinResult` | `hashtree.unpin` / `.result` | | `job` | `jobId` (`tstr`) | `HashtreeJobStatus` | `hashtree.job` / `.result` | | `cancel` | `jobId` (`tstr`) | `HashtreeJobStatus` | `hashtree.cancel` / `.result` | #### Schemas `RelayEventResult` is owned by NAP-RELAY. NAP-HASHTREE imports it by name for root events returned while resolving mutable Hashtree references. `publishRoot` still returns its shell-created signed event as `NostrEvent`. Primitive aliases and imported types: | Name | Definition | | --- | --- | | `HexHash` | hash encoded as lowercase hex | | `HexKey` | key material encoded as lowercase hex | | `HexPubkey` | 32-byte public key encoded as lowercase hex | | `NostrEvent` | Nostr event object | | `NostrEventId` | 32-byte event id encoded as lowercase hex | | `RelayEventResult` | external type owned by NAP-RELAY | Enums: | Name | Values | | --- | --- | | `HashtreeRefKind` | `nhash`, `cid`, `htree`, `npub-path` | | `HashtreeEntryType` | `blob`, `file`, `dir` | | `HashtreeVisibility` | `unencrypted`, `public`, `link-visible`, `private` | | `HashtreeJobKind` | `import`, `export` | | `HashtreeJobState` | `queued`, `running`, `complete`, `cancelled`, `error` | | `HashtreeExportDestination` | `user-selected`, `temporary` | Record fields: | Record | Required fields | Optional fields | | --- | --- | --- | | `HashtreeCapabilities` | none | `canReadImmutable: bool`, `canResolveMutable: bool`, `canWrite: bool`, `canPublishRoots: bool`, `canPin: bool`, `canImportRuntimeFiles: bool`, `canExportRuntimeFiles: bool`, `supportsFips: bool`, `supportsBlossom: bool`, `supportsLocalStore: bool`, `maxReadBytes: uint`, `maxWriteBytes: uint` | | `HashtreeCid` | `hash: HexHash` | `key: HexKey`, `nhash: tstr` | | `HashtreeRef` | `kind: HashtreeRefKind` | `ref: tstr`, `cid: HashtreeCid`, `identifier: tstr`, `treePath: tstr`, `entryPath: tstr`, `visibility: HashtreeVisibility`, `shareSecret: HexKey` | | `HashtreeRefResult` | `ok: bool` | `ref: HashtreeRef`, `error: tstr`, `reason: tstr` | | `HashtreeResolveOptions` | none | `shareSecret: HexKey`, `at: uint` | | `HashtreeResolvedRoot` | `cid: HashtreeCid` | `ref: HashtreeRef`, `rootResult: RelayEventResult`, `eventId: NostrEventId`, `author: HexPubkey`, `treeName: tstr`, `visibility: HashtreeVisibility` | | `HashtreeResolveResult` | `ok: bool` | `root: HashtreeResolvedRoot`, `error: tstr`, `reason: tstr` | | `HashtreeEntry` | `path: tstr`, `type: HashtreeEntryType`, `cid: HashtreeCid` | `name: tstr`, `size: uint`, `mime: tstr` | | `HashtreeEntryResult` | `ok: bool` | `entry: HashtreeEntry`, `error: tstr`, `reason: tstr` | | `HashtreeListOptions` | none | `recursive: bool`, `limit: uint` | | `HashtreeListResult` | `ok: bool`, `entries: list of HashtreeEntry` | `root: HashtreeResolvedRoot`, `truncated: bool`, `error: tstr`, `reason: tstr` | | `HashtreeReadOptions` | none | `offset: uint`, `length: uint`, `maxBytes: uint` | | `HashtreeReadResult` | `ok: bool` | `blob: bstr`, `entry: HashtreeEntry`, `root: HashtreeResolvedRoot`, `error: tstr`, `reason: tstr` | | `HashtreePutOptions` | none | `visibility: HashtreeVisibility`, `mime: tstr`, `filename: tstr`, `pin: bool` | | `HashtreePutBytesRequest` | `data: bstr` | `options: HashtreePutOptions` | | `HashtreeTreeEntryInput` | `path: tstr` | `data: bstr`, `cid: HashtreeCid`, `mime: tstr` | | `HashtreePutTreeRequest` | `entries: list of HashtreeTreeEntryInput` | `options: HashtreePutOptions` | | `HashtreePutResult` | `ok: bool` | `cid: HashtreeCid`, `entry: HashtreeEntry`, `size: uint`, `error: tstr`, `reason: tstr` | | `HashtreePublishRootRequest` | `cid: HashtreeCid`, `treeName: tstr` | `content: tstr`, `visibility: HashtreeVisibility`, `shareSecret: HexKey`, `relays: list of tstr` | | `HashtreePublishRootResult` | `ok: bool` | `eventId: NostrEventId`, `event: NostrEvent`, `ref: tstr`, `relays: map of relay URL to bool`, `error: tstr`, `reason: tstr` | | `HashtreeImportPublishOptions` | `treeName: tstr` | `content: tstr`, `visibility: HashtreeVisibility`, `shareSecret: HexKey`, `relays: list of tstr` | | `HashtreePickOptions` | none | `multiple: bool`, `directory: bool`, `suggestedName: tstr` | | `HashtreeRuntimeFileSet` | `runtimeFileSetId: tstr`, `files: list of HashtreeEntry`, `totalBytes: uint` | `displayName: tstr` | | `HashtreeFileSetResult` | `ok: bool` | `fileSet: HashtreeRuntimeFileSet`, `error: tstr`, `reason: tstr` | | `HashtreeImportRequest` | `runtimeFileSetId: tstr` | `visibility: HashtreeVisibility`, `pin: bool`, `publish: HashtreeImportPublishOptions` | | `HashtreeExportRequest` | `ref: tstr` | `destination: HashtreeExportDestination` | | `HashtreeJobAck` | `ok: bool` | `jobId: tstr`, `state: HashtreeJobState`, `error: tstr`, `reason: tstr` | | `HashtreeJobStatus` | `ok: bool`, `jobId: tstr`, `kind: HashtreeJobKind`, `state: HashtreeJobState`, `processedBytes: uint`, `totalBytes: uint`, `progress: number` | `cid: HashtreeCid`, `ref: tstr`, `runtimeFileSetId: tstr`, `error: tstr`, `reason: tstr` | | `HashtreePin` | `ref: tstr`, `cid: HashtreeCid` | `label: tstr`, `bytesStored: uint`, `createdAt: uint` | | `HashtreePinRequest` | `ref: tstr` | `label: tstr` | | `HashtreePinResult` | `ok: bool` | `pin: HashtreePin`, `error: tstr`, `reason: tstr` | When `HashtreePutOptions.visibility` is omitted, the default is `public`. **`capabilities()`** -- Reports the runtime's Hashtree support for this napplet. Backend flags describe runtime policy, not direct napplet access. **`parseRef(ref)`** -- Parses `nhash`, CID text (`` or `:`), `htree://`, and `npub/path` references without fetching data. `hashtree:nhash` MUST parse as the same immutable reference as `nhash`. **`resolve(ref, options?)`** -- Resolves immutable or mutable references to a root CID. For mutable references, the runtime queries Nostr root events, chooses the newest root using HTS-01 ordering, and derives public, link-visible, or private root keys when authorized. Returned mutable-root events use `RelayEventResult`. **`stat(ref)`** -- Returns metadata for a blob, file, or directory without returning file bytes. **`list(ref, options?)`** -- Lists directory entries. The runtime decodes and verifies Hashtree nodes before returning entries. `recursive` can be capped by runtime policy. **`read(ref, options?)`** -- Reads a blob or file. The runtime fetches chunks from local storage, Blossom, FIPS peers, or another configured backend, verifies hashes, decrypts when a key is available, and returns bytes. It MUST reject a directory read with `"is-directory"`. **`putBytes(request)`** -- Stores bytes as a Hashtree blob/file and returns its CID. The runtime applies HTS-01 chunking, node encoding, and optional CHK encryption. It MAY upload stored objects to configured remote stores. **`putTree(request)`** -- Builds a Hashtree directory from supplied bytes and/or existing CIDs. Paths are logical tree paths, not filesystem paths. The runtime normalizes and rejects traversal. **`publishRoot(request)`** -- Publishes a Nostr kind `30064` root event with `d`, `l`, and `hash` tags, plus the appropriate visibility tag. The runtime signs as the current shell-user and owns relay fanout. **`pickFiles(options?)`** -- Opens a runtime-owned file or directory picker and returns an opaque `runtimeFileSetId`. The handle can be imported, but it does not expose local paths or bytes to the napplet. **`importFiles(request)`** -- Stores a runtime-owned file set as a Hashtree tree. It is a long-running job because large directories may need chunking, hashing, uploading, pinning, and optional root publication. **`exportFiles(request)`** -- Materializes a Hashtree ref into a runtime-owned file set or user-selected destination. It is a long-running job because it may need network fetches, verification, decryption, and directory reconstruction. **`pins()` / `pin(request)` / `unpin(ref)`** -- Manage runtime retention for Hashtree CIDs. Pinning is a storage policy request, not a publication event. **`job(jobId)` / `cancel(jobId)`** -- Inspect or cancel import/export jobs. ### HTS-01 Mapping | HTS-01 concept | NAP-HASHTREE field or behavior | | --- | --- | | SHA-256 content address | `HashtreeCid.hash` | | CID key | `HashtreeCid.key` | | `nhash` | `HashtreeCid.nhash` and immutable `parseRef` output | | MessagePack file/dir node | runtime-owned implementation behind `putTree`, `list`, and `read` | | CHK encryption | `visibility`, `key`, `shareSecret`, and runtime encryption policy | | kind `30064` root event | `publishRoot`, `resolve`, `HashtreeResolvedRoot.rootResult` | | `d` tag | `treeName` | | `hash` tag | `HashtreeCid.hash` | | `key`, `encryptedKey`, `selfEncryptedKey` tags | root visibility and key recovery | | `htree:///` | mutable `HashtreeRef` | `htree://` tree names that contain `/` MUST follow HTS-01 URL encoding guidance: encode the tree name as one path segment before appending entry path segments. ### Wire Protocol `hashtree.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `hashtree.capabilities` | napplet -> runtime | `id` | | `hashtree.capabilities.result` | runtime -> napplet | `id`, `capabilities?`, `error?` | | `hashtree.parseRef` | napplet -> runtime | `id`, `ref` | | `hashtree.parseRef.result` | runtime -> napplet | `id`, `result` | | `hashtree.resolve` | napplet -> runtime | `id`, `ref`, `options?` | | `hashtree.resolve.result` | runtime -> napplet | `id`, `result` | | `hashtree.stat` | napplet -> runtime | `id`, `ref` | | `hashtree.stat.result` | runtime -> napplet | `id`, `result` | | `hashtree.list` | napplet -> runtime | `id`, `ref`, `options?` | | `hashtree.list.result` | runtime -> napplet | `id`, `result` | | `hashtree.read` | napplet -> runtime | `id`, `ref`, `options?` | | `hashtree.read.result` | runtime -> napplet | `id`, `result` | | `hashtree.putBytes` | napplet -> runtime | `id`, `request` | | `hashtree.putBytes.result` | runtime -> napplet | `id`, `result` | | `hashtree.putTree` | napplet -> runtime | `id`, `request` | | `hashtree.putTree.result` | runtime -> napplet | `id`, `result` | | `hashtree.publishRoot` | napplet -> runtime | `id`, `request` | | `hashtree.publishRoot.result` | runtime -> napplet | `id`, `result` | | `hashtree.pickFiles` | napplet -> runtime | `id`, `options?` | | `hashtree.pickFiles.result` | runtime -> napplet | `id`, `result` | | `hashtree.importFiles` | napplet -> runtime | `id`, `request` | | `hashtree.importFiles.result` | runtime -> napplet | `id`, `ack` | | `hashtree.exportFiles` | napplet -> runtime | `id`, `request` | | `hashtree.exportFiles.result` | runtime -> napplet | `id`, `ack` | | `hashtree.pins` | napplet -> runtime | `id` | | `hashtree.pins.result` | runtime -> napplet | `id`, `pins?`, `error?` | | `hashtree.pin` | napplet -> runtime | `id`, `request` | | `hashtree.pin.result` | runtime -> napplet | `id`, `result` | | `hashtree.unpin` | napplet -> runtime | `id`, `ref` | | `hashtree.unpin.result` | runtime -> napplet | `id`, `result` | | `hashtree.job` | napplet -> runtime | `id`, `jobId` | | `hashtree.job.result` | runtime -> napplet | `id`, `status?`, `error?` | | `hashtree.cancel` | napplet -> runtime | `id`, `jobId` | | `hashtree.cancel.result` | runtime -> napplet | `id`, `status?`, `error?` | | `hashtree.progress` | runtime -> napplet | `jobId`, `status` | | `hashtree.done` | runtime -> napplet | `jobId`, `status` | | `hashtree.error` | runtime -> napplet | `jobId`, `error`, `reason?` | Key design notes: - Request/result pairs use `id` for correlation. - Import/export push messages use `jobId`; they have no `id`. - Binary `data` and `blob` fields cross the boundary as projection-native byte objects. In the web projection that means `Blob` or `ArrayBuffer`, not base64 strings. - `runtimeFileSetId` is runtime-scoped. It is not a path and cannot be reused outside the grant that produced it. - Root publication and resolution are part of this domain. Generic Nostr relay access remains in NAP-RELAY. #### Examples **Resolve and read a public file:** ``` -> { "type": "hashtree.resolve", "id": "r1", "ref": "htree://npub1.../docs/readme.txt" } <- { "type": "hashtree.resolve.result", "id": "r1", "result": { "ok": true, "root": { "cid": { "hash": "ab12..." }, "treeName": "docs", "visibility": "unencrypted" } } } -> { "type": "hashtree.read", "id": "r2", "ref": "htree://npub1.../docs/readme.txt" } <- { "type": "hashtree.read.result", "id": "r2", "result": { "ok": true, "blob": "", "entry": { "path": "readme.txt", "type": "file", "cid": { "hash": "cd34..." }, "size": 1200, "mime": "text/plain" } } } ``` **Store bytes and publish a root:** ``` -> { "type": "hashtree.putBytes", "id": "p1", "request": { "data": "", "options": { "visibility": "public", "filename": "note.txt", "pin": true } } } <- { "type": "hashtree.putBytes.result", "id": "p1", "result": { "ok": true, "cid": { "hash": "ab12...", "key": "ef56...", "nhash": "nhash1..." }, "entry": { "path": "note.txt", "type": "file", "cid": { "hash": "ab12...", "key": "ef56..." }, "size": 512 } } } -> { "type": "hashtree.publishRoot", "id": "p2", "request": { "cid": { "hash": "ab12...", "key": "ef56..." }, "treeName": "notes", "visibility": "public" } } <- { "type": "hashtree.publishRoot.result", "id": "p2", "result": { "ok": true, "eventId": "event...", "ref": "htree://npub1.../notes" } } ``` **Import a user-selected directory:** ``` -> { "type": "hashtree.pickFiles", "id": "f1", "options": { "directory": true, "suggestedName": "site" } } <- { "type": "hashtree.pickFiles.result", "id": "f1", "result": { "ok": true, "fileSet": { "runtimeFileSetId": "fs-1", "files": [], "totalBytes": 7340032, "displayName": "site" } } } -> { "type": "hashtree.importFiles", "id": "i1", "request": { "runtimeFileSetId": "fs-1", "visibility": "link-visible", "pin": true, "publish": { "treeName": "site", "visibility": "link-visible" } } } <- { "type": "hashtree.importFiles.result", "id": "i1", "ack": { "ok": true, "jobId": "ht-1", "state": "queued" } } <- { "type": "hashtree.done", "jobId": "ht-1", "status": { "ok": true, "jobId": "ht-1", "kind": "import", "state": "complete", "processedBytes": 7340032, "totalBytes": 7340032, "progress": 1, "cid": { "hash": "ab12...", "key": "ef56..." }, "ref": "htree://npub1.../site#k=..." } } ``` ### Error Handling Common errors: `invalid-ref`, `invalid-cid`, `invalid-tree`, `invalid-path`, `not-found`, `missing-key`, `decrypt-failed`, `unsupported-visibility`, `not-signed-in`, `user-denied`, `relay-timeout`, `publish-failed`, `fetch-failed`, `store-failed`, `too-large`, `quota-exceeded`, `is-directory`, `not-directory`, `file-pick-cancelled`, `job-not-found`, `policy-denied`, `unsupported`. The runtime SHOULD return structured `ok: false` results when the request was understood but refused. It SHOULD use top-level `error` only when no typed result can be constructed. ### Runtime Behavior - MUST validate all returned bytes by SHA-256 before delivering them. - MUST reject deterministic MessagePack tree nodes that violate HTS-01 ordering or type rules. - MUST keep CHK keys, link-visible share secrets, and private root keys under runtime policy. - MUST construct kind `30064` root events with `d`, `l`, and `hash` tags. - MUST sign root events as the current shell-user. - MUST choose relays, Blossom servers, FIPS peers, local caches, and fallback order by runtime policy. - SHOULD merge observed mutable-root relay URLs into `RelayEventResult.sidecar.relayHints` when it can disclose them. - MAY include pre-resolved resources referenced by mutable-root events in `RelayEventResult.sidecar.resources`; NAP-RELAY's default-off sidecar privacy policy and NAP-RESOURCE's fetch policy both apply. - MUST NOT expose raw Blossom, FIPS, filesystem, or signing access through this interface. - MUST sanitize logical paths and reject absolute paths, `..`, and equivalent traversal. - MUST scope pins, jobs, and runtime file handles to the requesting napplet unless the user explicitly grants broader access. - SHOULD cap recursive listing, read size, import size, export size, and concurrent jobs. - MAY support legacy root kind `30078` when resolving, as HTS-01 permits. ### Security Considerations - A CID with a CHK key is a read capability. Runtimes SHOULD avoid exposing keys unless needed by the napplet and authorized by the user. - Link-visible roots carry a share secret in a fragment. Runtimes MUST NOT send that secret to relays, Blossom servers, FIPS peers, or HTTP request bodies. - Private roots require user identity and NIP-44 decryption. Runtimes MUST reject private resolution when no authorized signer/decrypter is connected. - Importing local files can exfiltrate user data. Consent UI SHOULD show file count, approximate bytes, visibility mode, publication target, and retention policy before starting. - Exporting can materialize untrusted filenames. Runtimes MUST sanitize names and prevent traversal outside the chosen destination. - FIPS and Blossom fetches can leak interest. Runtimes SHOULD disclose network path policy when resolving remote roots. - Pins consume storage and may retain sensitive encrypted blobs. Runtimes SHOULD make pin scope and retention visible. ### References - [Hashtree](https://github.com/mmalmi/hashtree) - [HTS-01: hashtree Core Protocol](https://github.com/mmalmi/hashtree/blob/master/docs/HTS-01.md) - [Hashtree URL Encoding](https://github.com/mmalmi/hashtree/blob/master/docs/URL-ENCODING.md) - [Hashtree on FIPS](https://github.com/mmalmi/hashtree/blob/master/docs/hashtree-on-fips.md) ### Implementations - None yet. ### Changelog - `08c0396` - Introduced NAP-HASHTREE for runtime-mediated Hashtree document trees. - `3847b0f` - Changed resolved Hashtree root events to use relay-owned result shape and declared the relay dependency. - `623b44c` - Adopted injected-domain availability and linked the current upstream web binding. [Join the discussion · PR #73 ↗](https://github.com/napplet/naps/pull/73)[Read proposed changes ↗](https://github.com/napplet/naps/pull/73/files) On this pageSpecification metadata Capability domain `hashtree` Repository state Draft PR Registry status Not listed in the registry Document marker draft Declared dependencies - [identity](https://napplet.run/protocol/nap-identity/) - [relay](https://napplet.run/protocol/nap-relay-pr-2/) - [resource](https://napplet.run/protocol/nap-resource-pr-80/) Source revision `67997706ac62` [View source on GitHub ↗](https://github.com/napplet/naps/pull/73/files) --- [← All NAPs](https://napplet.run/protocol/) NAP-IDENTITY / Merged specification # Read-Only User Identity Queries Merged **Shipped implementation: supported** Read-only identity queries and updates have focused binding and reference-runtime coverage. Implementation proposal: [merged ↗](https://github.com/napplet/naps/pull/102), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/naps/NAP-IDENTITY.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md). ## NAP-IDENTITY ### Read-Only User Identity Queries `draft` **NAP ID:** NAP-IDENTITY **Domain:** `identity` **Depends:** - `resource` — capability · optional — `picture` / `banner` URL bytes are fetched via `resource.bytes` **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.identity`; domain presence signals availability. ### Description NAP-IDENTITY provides read-only access to the shell-user identity: the currently connected user/signer pubkey and related public identity data. Napplets that need to display the user's profile, check their follow list, or read their relay preferences query the shell for this data. The shell resolves these queries against its local cache or connected relays and returns the results. Napplets do not have direct access to the user's private key. They cannot sign events, encrypt, or decrypt. Identity queries are strictly read-only -- napplets learn *about* the user but cannot act *as* the user. Signing is delegated to the shell via `relay.publish()` (NAP-RELAY). Encryption is delegated via `relay.publishEncrypted()`. This shell-user identity is distinct from napplet identity. The runtime binds an authenticated endpoint to its verified manifest and artifact before execution, as defined by the [web projection](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/projections/web.md). NAP-IDENTITY only reports the user's connected signer identity; it does not establish or change the napplet's identity. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `getPublicKey` | none | `tstr` public key, or `""` when no signer is connected | `identity.getPublicKey` / `identity.getPublicKey.result` | | `onChanged` | handler for `tstr` public key | `Subscription` handle | `identity.changed` | | `getRelays` | none | `RelayMap` | `identity.getRelays` / `identity.getRelays.result` | | `getProfile` | none | `ProfileData` or `null` | `identity.getProfile` / `identity.getProfile.result` | | `getFollows` | none | list of `tstr` pubkeys | `identity.getFollows` / `identity.getFollows.result` | | `getList` | `type` (`tstr`) | list of `tstr` entries | `identity.getList` / `identity.getList.result` | | `getZaps` | none | list of `ZapReceipt` | `identity.getZaps` / `identity.getZaps.result` | | `getMutes` | none | list of `tstr` pubkeys | `identity.getMutes` / `identity.getMutes.result` | | `getBlocked` | none | list of `tstr` pubkeys | `identity.getBlocked` / `identity.getBlocked.result` | | `getBadges` | none | list of `Badge` | `identity.getBadges` / `identity.getBadges.result` | #### Schemas `RelayPermissions` fields: | Field | Required | Type | | --- | --- | --- | | `read` | yes | boolean | | `write` | yes | boolean | `RelayMap` is a map from relay URL text to `RelayPermissions`. `ProfileData` fields: | Field | Required | Type | | --- | --- | --- | | `name` | no | text | | `displayName` | no | text | | `about` | no | text | | `picture` | no | text | | `banner` | no | text | | `nip05` | no | text | | `lud16` | no | text | | `website` | no | text | **Resource resolution.** `picture` and `banner` are URL strings. Napplets that need their bytes MUST use `resource.bytes` when the optional `resource` domain is exposed, and SHOULD use local placeholder artwork when it is absent. The [web projection](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/projections/web.md) applies the runtime's network policy; an opaque sandbox origin alone does not block network requests. URLs do not grant network authority. Standard resource policy applies to mediated fetches. `ZapReceipt` fields: | Field | Required | Type | | --- | --- | --- | | `eventId` | yes | text | | `sender` | yes | text | | `amount` | yes | unsigned integer | | `content` | no | text | `Badge` fields: | Field | Required | Type | | --- | --- | --- | | `id` | yes | text | | `name` | no | text | | `description` | no | text | | `image` | no | text | | `thumbs` | no | list of text | | `awardedBy` | yes | text | **`getPublicKey()`** -- Returns the user's hex-encoded public key, or the empty string when no user/signer is connected. This is the most basic identity query. Every shell that implements NAP-IDENTITY MUST support this method. **`onChanged(handler)`** -- Registers a handler for shell-pushed user identity changes. The handler receives the same public-key string shape as `getPublicKey()`: a non-empty hex pubkey when a user/signer is connected, or `""` when the shell signs out or disconnects the signer. **`getRelays()`** -- Returns the user's relay list as a record mapping relay URLs to read/write permissions (NIP-65 relay list metadata). **`getProfile()`** -- Returns the user's kind 0 profile metadata. Returns `null` if no profile is found. The shell resolves this from its cache or relays. **`getFollows()`** -- Returns an array of hex-encoded public keys from the user's kind 3 contact list. **`getList(type)`** -- Returns entries from a user's categorized list. The `type` parameter specifies the list kind (e.g., `"bookmarks"`, `"interests"`, `"pins"`). Returns the tag values from the matching parameterized replaceable event. **`getZaps()`** -- Returns zap receipts (kind 9735) sent to the user. Each receipt includes the sender, amount in millisats, and optional content. **`getMutes()`** -- Returns the user's mute list (kind 10000) as an array of hex-encoded public keys. **`getBlocked()`** -- Returns the user's block list as an array of hex-encoded public keys. Semantically distinct from mutes -- blocks are hard exclusions. **`getBadges()`** -- Returns badges awarded to the user (NIP-58). Each badge includes the badge definition ID, metadata, and the awarding pubkey. ### Wire Protocol `identity.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `identity.getPublicKey` | napplet -> shell | `id` | | `identity.getPublicKey.result` | shell -> napplet | `id`, `pubkey` | | `identity.changed` | shell -> napplet | `pubkey` | | `identity.getRelays` | napplet -> shell | `id` | | `identity.getRelays.result` | shell -> napplet | `id`, `relays`, `error?` | | `identity.getProfile` | napplet -> shell | `id` | | `identity.getProfile.result` | shell -> napplet | `id`, `profile?`, `error?` | | `identity.getFollows` | napplet -> shell | `id` | | `identity.getFollows.result` | shell -> napplet | `id`, `pubkeys`, `error?` | | `identity.getList` | napplet -> shell | `id`, `listType` | | `identity.getList.result` | shell -> napplet | `id`, `entries`, `error?` | | `identity.getZaps` | napplet -> shell | `id` | | `identity.getZaps.result` | shell -> napplet | `id`, `zaps`, `error?` | | `identity.getMutes` | napplet -> shell | `id` | | `identity.getMutes.result` | shell -> napplet | `id`, `pubkeys`, `error?` | | `identity.getBlocked` | napplet -> shell | `id` | | `identity.getBlocked.result` | shell -> napplet | `id`, `pubkeys`, `error?` | | `identity.getBadges` | napplet -> shell | `id` | | `identity.getBadges.result` | shell -> napplet | `id`, `badges`, `error?` | Key design notes: - All methods are request/result pairs using `id` for correlation. - `identity.changed` is a push message. It has no `id` and is not a response to a request. - All results include an optional `error` field for failure cases. - `getPublicKey` is the simplest query and MUST always succeed (no `error` field). It returns `pubkey: ""` when no shell-user identity is currently connected. - `getProfile` returns `profile: null` if no kind 0 is found -- this is not an error. - `getList` includes a `listType` field to specify which categorized list to query. Recommended handshake: 1. A napplet that needs user identity calls `identity.getPublicKey` once on startup to get a snapshot. 2. The shell returns `identity.getPublicKey.result` with `pubkey` set to either a hex pubkey or `""`. 3. The napplet then listens for `identity.changed` and re-runs identity-dependent work when the pushed pubkey changes. Napplets SHOULD NOT poll `identity.getPublicKey` waiting for a signer to appear. Shells MUST use `identity.changed` to notify already-loaded napplets when the shell-user identity changes. #### Examples **Get public key:** ``` -> { "type": "identity.getPublicKey", "id": "q1" } <- { "type": "identity.getPublicKey.result", "id": "q1", "pubkey": "ab12cd..." } ``` **No user connected:** ``` -> { "type": "identity.getPublicKey", "id": "q1b" } <- { "type": "identity.getPublicKey.result", "id": "q1b", "pubkey": "" } ``` **User identity changed:** ``` <- { "type": "identity.changed", "pubkey": "ab12cd..." } <- { "type": "identity.changed", "pubkey": "" } ``` **Get profile:** ``` -> { "type": "identity.getProfile", "id": "q2" } <- { "type": "identity.getProfile.result", "id": "q2", "profile": { "name": "Alice", "about": "Nostr dev", "picture": "https://..." } } ``` **Profile not found:** ``` -> { "type": "identity.getProfile", "id": "q3" } <- { "type": "identity.getProfile.result", "id": "q3", "profile": null } ``` **Get follows:** ``` -> { "type": "identity.getFollows", "id": "q4" } <- { "type": "identity.getFollows.result", "id": "q4", "pubkeys": ["ab12...", "cd34...", "ef56..."] } ``` **Get relay list:** ``` -> { "type": "identity.getRelays", "id": "q5" } <- { "type": "identity.getRelays.result", "id": "q5", "relays": { "wss://relay.example.com": { "read": true, "write": true }, "wss://readonly.example.com": { "read": true, "write": false } } } ``` **Get categorized list:** ``` -> { "type": "identity.getList", "id": "q6", "listType": "bookmarks" } <- { "type": "identity.getList.result", "id": "q6", "entries": ["note1abc...", "note1def..."] } ``` **Get mutes:** ``` -> { "type": "identity.getMutes", "id": "q7" } <- { "type": "identity.getMutes.result", "id": "q7", "pubkeys": ["spam1...", "spam2..."] } ``` **Get zap receipts:** ``` -> { "type": "identity.getZaps", "id": "q8" } <- { "type": "identity.getZaps.result", "id": "q8", "zaps": [{ "eventId": "ev1...", "sender": "ab12...", "amount": 21000, "content": "Great post!" }] } ``` **Get badges:** ``` -> { "type": "identity.getBadges", "id": "q9" } <- { "type": "identity.getBadges.result", "id": "q9", "badges": [{ "id": "badge1...", "name": "Early Adopter", "awardedBy": "cd34..." }] } ``` **Error case:** ``` -> { "type": "identity.getFollows", "id": "q10" } <- { "type": "identity.getFollows.result", "id": "q10", "pubkeys": [], "error": "relay timeout" } ``` #### Error Handling Result messages include an `error` field (string) when the shell cannot fulfill the request. Common errors: `"relay timeout"`, `"not found"`, `"unsupported list type"`. When `error` is present, the primary result field contains a sensible default (empty array, null, etc.). `identity.getPublicKey.result` MUST NOT include an `error` field. The shell returns `pubkey: ""` when no user/signer is connected. ### Shell Behavior - The shell MUST respond to every `identity.*` request with the corresponding `*.result` message carrying the same `id`. - The shell MUST return `pubkey` for `identity.getPublicKey` without error. If no user/signer is connected, `pubkey` MUST be the empty string. - The shell MUST emit `identity.changed` to loaded napplets whenever the shell-user identity changes. - The shell MUST emit `identity.changed` with `pubkey: ""` when the shell-user identity is cleared, the signer disconnects, or the user signs out. - The shell MAY emit an initial `identity.changed` after napplet load, but napplets MUST NOT depend on that initial push and SHOULD use `identity.getPublicKey` for the startup snapshot. - The shell SHOULD resolve identity queries from its local cache when available. - The shell MAY fetch data from relays if not cached. - The shell MAY return stale data with a best-effort freshness policy. - The shell MAY enforce ACL checks on identity capabilities (e.g., restricting which napplets can query follows or mutes). - The shell MAY return empty results for queries it does not support (e.g., badges) rather than an error. - The shell MUST NOT expose the user's private key or any signing capability through this interface. - The shell MUST NOT provide encrypt or decrypt operations through this interface. ### Security Considerations - NAP-IDENTITY is strictly read-only. No method modifies user state, signs events, or performs cryptographic operations. - `identity.changed` reports only the shell-user identity. It does not change or renegotiate the napplet's [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) session identity. - The user's public key is not secret, but follow lists, mute lists, and block lists reveal social graph information. Shells MAY restrict access to sensitive lists based on napplet trust level. - Zap receipts reveal financial information. Shells SHOULD consider whether to expose zap data to all napplets or restrict it to trusted napplets. - Profile data may contain URLs (picture, banner, website). Napplets that render these URLs should sanitize them. The shell is not responsible for sanitizing profile content. - This interface intentionally excludes signing, encryption, and decryption. These operations are security-critical and belong to the shell. See NAP-RELAY for shell-mediated publish and encrypted publish. - Block lists and mute lists are sensitive. A malicious napplet could use this data to target blocked users through side channels. Shells MAY redact or restrict access to these lists. ### Implementations - (none yet) ### Changelog - `a802d35` - Introduced read-only user identity queries and optional resource fetching. - `06b1a1a` - Adopted injected-domain availability, verified manifest and artifact identity, and graceful fallback for optional resource fetching. [Read specification on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md) On this pageSpecification metadata Capability domain `identity` Repository state Merged Registry status Draft Document marker draft Declared dependencies - [resource](https://napplet.run/protocol/nap-resource-pr-80/) Source revision `43cb3a4e8555` [View source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md) --- [← All NAPs](https://napplet.run/protocol/) NAP-INC / Merged specification # Inter-Napplet Communication Merged **Shipped implementation: supported** Topic and channel bindings have compatibility tests and authenticated-endpoint reference-runtime coverage. Implementation proposal: [merged ↗](https://github.com/napplet/naps/pull/104), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/naps/NAP-INC.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md). ## NAP-INC ### Inter-Napplet Communication `draft` **NAP ID:** NAP-INC **Domain:** `inc` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.inc`; domain presence signals availability. ### Description NAP-INC provides topic-based publish/subscribe and point-to-point channels for communication between napplets. The contract is inter-napplet rather than inter-frame: [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) uses sandboxed iframes and `postMessage` today, but the INC API describes shell-mediated communication between napplet endpoints and does not require every implementation target to be modeled as a browser frame. Under the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) iframe transport, sandboxed napplets cannot communicate directly because the `allow-same-origin` sandbox token is absent -- they have opaque origins with no shared context. The shell routes messages between napplets using typed `inc.*` messages over the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format. Topic-based INC is loose coupling: the sender does not know who (if anyone) receives the message. Channel-based INC is tight coupling: a napplet opens a named connection to a specific peer and the shell validates the target once on open. The shell MAY also use INC topics for internal coordination (state operations, service commands, configuration). ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `emit` | `topic` or convention URI (`tstr`), optional `payload` (`any`) | none | `inc.emit` | | `on` | `topic` (`tstr`), event handler for `IncEvent` | `Subscription` handle | `inc.subscribe` / `inc.subscribe.result` | | `channel.open` | `target` (`tstr`, peer endpoint identifier) | `ChannelHandle` | `inc.channel.open` / `inc.channel.open.result` | | `channel.onOpened` | handler for inbound `ChannelHandle` | `Subscription` handle | `inc.channel.opened` | | `channel.list` | none | list of `ChannelInfo` | `inc.channel.list` / `inc.channel.list.result` | | `channel.broadcast` | `payload` (`any`) | none | `inc.channel.broadcast` | | `ChannelHandle.emit` | `payload` (`any`) | none | `inc.channel.emit` | | `ChannelHandle.on` | event handler for `ChannelEvent` | `Subscription` handle | `inc.channel.event` | | `ChannelHandle.onClosed` | handler for `ChannelClosed` | `Subscription` handle | `inc.channel.closed` | | `ChannelHandle.close` | none | none | `inc.channel.close` | #### Schemas `IncEvent` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `topic` | yes | text | Opaque topic name. Matched by exact string equality. | | `sender` | yes | text | Runtime-attested emitting napplet endpoint identifier. | | `payload` | no | any | Per-message data. | `ChannelHandle` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Shell-assigned channel id. | | `peer` | yes | text | Peer endpoint identifier. | `ChannelEvent` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `channelId` | yes | text | Shell-assigned channel id. | | `sender` | yes | text | Sender endpoint identifier. | | `payload` | no | any | Channel payload. | `ChannelClosed` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `channelId` | yes | text | Shell-assigned channel id. | | `reason` | no | text | Runtime-supplied terminal reason. | `ChannelInfo` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Shell-assigned channel id. | | `peer` | yes | text | Peer endpoint identifier. | `target`, `sender`, and `peer` are runtime-assigned endpoint identifiers. They are opaque text, unique to a live endpoint within the runtime, and MUST NOT be interpreted as a manifest `d` tag. The runtime MUST distinguish different publishers, manifests, and simultaneous instances. It MUST bind each identifier to the authenticated endpoint and its verified manifest and artifact, including root and snapshot manifests without a `d` tag. An identifier MUST NOT be reused for another endpoint during the runtime session. A channel remains bound to the endpoint authorized at open; a restart or replacement requires a new channel. Callers obtain peer identifiers from runtime-attested INC events or an explicit runtime-authorized peer selection. These identifiers do not imply catalog membership or installation permission, and are not interchangeable with catalog identifiers exposed by other domains. Identifier encoding is runtime policy. `id` and `channelId` are separate shell-assigned opaque channel identifiers. **`emit(topic, payload?)`** — Broadcasts a message to all napplets subscribed to the given topic. A napplet MAY supply a convention URI as `topic`; the runtime transposes its query parameters into `payload` before routing. Fire-and-forget — there is no delivery confirmation. The runtime derives `sender` from the authenticated emitting endpoint and includes its endpoint identifier in delivered events. The emitter cannot set or override `sender`. **`on(topic, callback)`** — Subscribes to messages on a topic. The callback receives an `IncEvent` with the topic, sender endpoint identifier, and payload. Returns a `Subscription` handle with a `close()` method to unsubscribe. Multiple subscriptions to the same topic are independent. #### Convention URI transposition The developer-facing convention URI is `napplet:/[...?params]`. Its path is the stable topic. Its query is shallow payload sugar. When `emit` receives a convention URI with a query, the runtime MUST: 1. Remove the query from the routed topic. 2. Percent-decode each unique `name=value` pair as text. 3. Place those pairs in a map of text to text and use that map as `payload`. The runtime MUST NOT coerce query values to boolean, number, or null. A `+` is a literal plus sign, not a space. A convention URI with a fragment, malformed percent-encoding, a repeated name, or an explicit `payload` argument alongside query parameters MUST be rejected before emission. Structured or non-text data MUST use the explicit `payload` argument with a queryless topic. This transposition is part of the `emit` operation. Topic routing still uses exact equality over the resulting stable topic. **`channel.open(target)`** — Opens a point-to-point channel to a napplet identified by its endpoint identifier. The shell validates the target and checks ACL on open. The opener receives a `ChannelHandle`. Before reporting success, the runtime MUST enqueue `inc.channel.opened` for the target. If the target is not found, has no live endpoint, cannot receive the notification, or is ACL-denied, the request fails. The shell validates once on open. Subsequent messages flow without per-message authorization. **`channel.onOpened(callback)`** — Receives a `ChannelHandle` when a peer opens a channel to this napplet. The handle identifies the same channel as the opener's handle and supports the same `emit`, `on`, `onClosed`, and `close` operations. Its `peer` is the runtime-attested opener endpoint identifier. This notification is not an authorization prompt. The runtime has already applied channel ACL. **`channel.list()`** — Returns informational snapshots of this napplet's active inbound and outbound channels. `ChannelInfo` is not a handle and does not attach the caller to a channel. **`channel.broadcast(payload)`** — Sends a message to all open channel peers at once. Fire-and-forget. **`ChannelHandle.emit(payload)`** — Sends a message to the channel peer. Fire-and-forget. **`ChannelHandle.on(callback)`** — Receives messages from the channel peer. The callback receives a `ChannelEvent` with `channelId`, sender endpoint identifier, and `payload`. **`ChannelHandle.onClosed(callback)`** — Receives the terminal `ChannelClosed` record when either endpoint closes the channel or the runtime tears it down. A handler registered after closure MUST be invoked with the retained terminal record. **`ChannelHandle.close()`** — Tears down the channel. Either endpoint may close. Both handles become terminal and their `onClosed` handlers are notified via `inc.channel.closed`. ### Wire Protocol INC operations use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). #### Topics | Type | Direction | Payload fields | | --- | --- | --- | | `inc.emit` | napplet -> shell | `topic`, `payload`? | | `inc.subscribe` | napplet -> shell | `id`, `topic` | | `inc.subscribe.result` | shell -> napplet | `id` | | `inc.unsubscribe` | napplet -> shell | `topic` | | `inc.event` | shell -> napplet | `topic`, `sender` (endpoint identifier), `payload`? | Key design notes: - `inc.emit` has no `id` field — it is fire-and-forget with no acknowledgment. - `inc.emit` carries the stable topic and transposed payload, not the developer-facing convention URI. - `inc.subscribe` uses `id` for correlation so the shim can confirm the subscription was registered. - `inc.subscribe.result` confirms registration by echoing the `id`. - `inc.unsubscribe` is fire-and-forget (no `id`, no result message). - `inc.event` has no `id` field — it is a shell-initiated delivery identified by `topic` and `sender` (the emitting napplet's endpoint identifier). #### Channels | Type | Direction | Payload fields | | --- | --- | --- | | `inc.channel.open` | napplet -> shell | `id`, `target` (endpoint identifier string) | | `inc.channel.open.result` | shell -> napplet | `id`, `channelId`?, `peer`? (endpoint identifier), `error`? | | `inc.channel.opened` | shell -> target napplet | `channelId`, `peer` (endpoint identifier) | | `inc.channel.emit` | napplet -> shell | `channelId`, `payload`? | | `inc.channel.event` | shell -> napplet | `channelId`, `sender` (endpoint identifier), `payload`? | | `inc.channel.broadcast` | napplet -> shell | `payload`? | | `inc.channel.list` | napplet -> shell | `id` | | `inc.channel.list.result` | shell -> napplet | `id`, `channels` (ChannelInfo array) | | `inc.channel.close` | napplet -> shell | `channelId` | | `inc.channel.closed` | shell -> napplet | `channelId`, `reason`? (string) | Key design notes: - `inc.channel.open` uses `id` for correlation; the result returns a shell-assigned `channelId`. - `inc.channel.opened` is a target-side push with no request `id`. It carries the same `channelId`; `peer` is the runtime-attested opener endpoint identifier. - The runtime MUST enqueue `inc.channel.opened` for the target before sending a successful `inc.channel.open.result` to the opener. - For each endpoint, handle creation MUST precede any `inc.channel.event` or `inc.channel.closed` for that channel. - `inc.channel.emit` has no `id` field — fire-and-forget like `inc.emit`. - `inc.channel.event` has no `id` field — shell-initiated delivery, carries `channelId` + `sender`. - `inc.channel.broadcast` has no `id` field — fire-and-forget to all open channel peers. - `inc.channel.close` has no `id` — the `channelId` identifies the target channel. - `inc.channel.closed` is a shell-initiated notification sent on graceful close, peer destruction, or ACL revocation. #### Examples **Emit a convention URI:** ``` emit("napplet:profile/open?pubkey=abc123...") ``` The runtime transposes the call before sending the wire message: ``` -> { "type": "inc.emit", "topic": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } ``` No response — fire-and-forget. **Subscribe:** ``` -> { "type": "inc.subscribe", "id": "a1", "topic": "napplet:profile/open" } <- { "type": "inc.subscribe.result", "id": "a1" } ``` **Event delivery:** ``` <- { "type": "inc.event", "topic": "napplet:profile/open", "sender": "endpoint-social-feed", "payload": { "pubkey": "abc123..." } } ``` **Unsubscribe:** ``` -> { "type": "inc.unsubscribe", "topic": "napplet:profile/open" } ``` **Open channel:** Assume the opener endpoint identifier is `endpoint-music-controller` and the target endpoint identifier is `endpoint-media-player`: ``` opener -> { "type": "inc.channel.open", "id": "ch1", "target": "endpoint-media-player" } target <- { "type": "inc.channel.opened", "channelId": "c-abc", "peer": "endpoint-music-controller" } opener <- { "type": "inc.channel.open.result", "id": "ch1", "channelId": "c-abc", "peer": "endpoint-media-player" } ``` The opener and target bindings each materialize a `ChannelHandle` for `c-abc`. **Channel emit:** ``` opener -> { "type": "inc.channel.emit", "channelId": "c-abc", "payload": { "command": "play", "track": 3 } } target <- { "type": "inc.channel.event", "channelId": "c-abc", "sender": "endpoint-music-controller", "payload": { "command": "play", "track": 3 } } ``` No response — fire-and-forget. **Target reply:** ``` target -> { "type": "inc.channel.emit", "channelId": "c-abc", "payload": { "status": "playing", "track": 3 } } opener <- { "type": "inc.channel.event", "channelId": "c-abc", "sender": "endpoint-media-player", "payload": { "status": "playing", "track": 3 } } ``` **Channel broadcast:** ``` -> { "type": "inc.channel.broadcast", "payload": { "announcement": "shutting down" } } ``` **List channels:** ``` -> { "type": "inc.channel.list", "id": "l1" } <- { "type": "inc.channel.list.result", "id": "l1", "channels": [{ "id": "c-abc", "peer": "endpoint-media-player" }, { "id": "c-def", "peer": "endpoint-chat-widget" }] } ``` **Close channel:** ``` -> { "type": "inc.channel.close", "channelId": "c-abc" } <- { "type": "inc.channel.closed", "channelId": "c-abc" } ``` **Channel closed by peer or shell:** ``` <- { "type": "inc.channel.closed", "channelId": "c-abc", "reason": "peer destroyed" } ``` **Open error:** ``` <- { "type": "inc.channel.open.result", "id": "ch1", "error": "target not found" } ``` #### Error Handling `inc.subscribe.result` MAY include an `error` field (string) if the shell rejects the subscription. When `error` is present, the subscription was not registered. ``` <- { "type": "inc.subscribe.result", "id": "a1", "error": "topic rejected by ACL" } ``` `inc.channel.open.result` MAY include an `error` field if the channel cannot be opened. When `error` is present, no channel was created. A successful `inc.channel.open.result` means the runtime created channel state for both endpoints and enqueued the target's `inc.channel.opened` notification. It does not mean the target registered an application handler or accepted the channel's application semantics. ### Topic Conventions Topics use a prefix convention to signal direction and scope: | Prefix | Direction | Meaning | | --- | --- | --- | | `shell:*` | napplet -> shell | Commands sent by a napplet to the shell (e.g., `shell:state-get`) | | `napplet:/` | bidirectional | Archetype-scoped messages between napplets (e.g., `napplet:profile/open`, `napplet:dm/open`) | The routed topic is the stable message identity. The payload carries per-message data. A convention producer MAY use the full convention URI when calling `emit`; the runtime MUST transpose it before routing. Consumers subscribe to the stable topic without query parameters. These prefixes are advisory. Except for an explicitly intercepted shell topic, the shell treats the complete topic as opaque text. It MUST NOT parse, decode, normalize, prefix-match, or wildcard-match a topic for routing. Therefore query transposition happens before topic routing, never as part of matching. ### Shell Behavior #### Topic routing - The shell MUST route `inc.emit` messages to all napplets subscribed to the exact same complete topic string. - The shell MUST copy the emitted `topic` unchanged into each delivered `inc.event`. - The runtime MUST derive `sender` from the authenticated emitting endpoint and include its endpoint identifier in delivered `inc.event` messages. It MUST ignore or reject caller-supplied sender data. - The shell MUST NOT deliver `inc.event` back to the emitting napplet (sender exclusion). - The shell MUST respond to `inc.subscribe` with `inc.subscribe.result` carrying the same `id`. - The shell MUST honor `inc.unsubscribe` by removing the subscription for that topic. - The shell MAY intercept specific topic prefixes (e.g., `shell:*`) for internal command handling rather than routing them to other napplets. - The shell MAY enforce ACL checks on INC capabilities and reject subscriptions or emits that violate shell policy. #### Channel management - The shell MUST respond to `inc.channel.open` with `inc.channel.open.result` carrying the same `id`. - The shell MUST assign a `channelId` (opaque identifier) on successful channel open. - The shell MUST validate that the target endpoint identifier exists and has a live napplet endpoint before opening a channel. - The runtime MUST derive the opener endpoint identifier from its authenticated endpoint. The opener cannot set or override `peer` in `inc.channel.opened`. - The runtime MUST enqueue `inc.channel.opened` for the target before it sends a successful `inc.channel.open.result` to the opener. - The runtime-provided binding MUST materialize a `ChannelHandle` from `inc.channel.opened` and retain it until at least one `channel.onOpened` handler receives it. - The runtime-provided binding MUST retain incoming `inc.channel.event` messages until at least one `ChannelHandle.on` handler is registered for that channel. It MUST NOT silently discard buffered channel messages. - The runtime-provided binding MUST retain the terminal `inc.channel.closed` record. A later `ChannelHandle.onClosed` registration MUST receive it. - A runtime MAY bound unopened-handle or message buffers. On overflow it MUST close the channel and notify both endpoints instead of silently dropping the handle or messages. - The shell MUST forward `inc.channel.emit` messages to the channel peer as `inc.channel.event`. - The shell MUST NOT perform per-message validation on channel messages after open — only the initial open is validated once (auth-on-open model). - The shell MUST send `inc.channel.closed` to both sides when a channel is torn down (graceful close, iframe removal, or ACL revocation). - The shell MUST respond to `inc.channel.list` with `inc.channel.list.result` listing the napplet's active channels. - The shell MUST deliver `inc.channel.broadcast` to all open channel peers of the sender, excluding the sender itself. - The shell MUST clean up channel state when a napplet endpoint is destroyed, sending `inc.channel.closed` with `reason: "peer destroyed"` to the surviving endpoint. - The shell MAY enforce ACL checks on `inc.channel.open` (e.g., restrict which endpoint identifiers a napplet can open channels to). - The shell MAY impose a maximum number of concurrent channels per napplet. ### Channels vs Topics Topics and channels serve different communication patterns within NAP-INC: **Topics** are loose-coupled publish/subscribe. Any napplet can subscribe to any topic; the sender does not know who (if anyone) will receive the message. There is no persistent connection. Topics are well-suited for infrequent coordination — UI commands, state sync, configuration events, and notifications. **Channels** are pre-authorized point-to-point connections. A napplet calls `channel.open(target)` to establish a channel to a specific peer identified by its endpoint identifier. The shell validates the target once on open (auth-on-open model): it checks that the target exists, has a live napplet endpoint, and passes ACL. After open, messages flow between the two endpoints without per-message shell validation. Channels are well-suited for sustained data streams, real-time collaboration, and command channels between specific napplets. Both are part of the same `inc` namespace and share the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format. A napplet may use both simultaneously. ### Security Considerations Sender identity is runtime-enforced from the authenticated emitting endpoint. There is no per-message signing. Runtime sender identification is the trust boundary. A projection defines how its authenticated endpoint is bound. #### Topics - Topic namespaces are not enforced — any napplet can emit on any topic. The shell MAY restrict topics via ACL. - After convention URI transposition, payloads are opaque to topic routing. Receiving napplets are responsible for validating payload content. - Sender exclusion prevents echo loops but does not prevent a napplet from emitting messages on any topic. Receivers should check the `sender` endpoint identifier if sender identity matters for their use case. #### Channels - Channel authorization is validated once on `inc.channel.open`. After open, messages flow without per-message shell validation. A compromised browser extension could forge messages on an open channel — this is an accepted trust boundary, the same as topic-based INC. - An inbound `ChannelHandle` means runtime ACL permitted the channel. It does not imply application-level consent. The target MAY close it immediately. - Unclaimed inbound handles and buffered messages consume runtime resources. The runtime SHOULD rate-limit opens and bound buffers per napplet. - Channel IDs are opaque. Napplets cannot enumerate or guess other channels' IDs. This prevents channel hijacking. - `inc.channel.broadcast` reaches all open channel peers. Napplets SHOULD NOT send sensitive data via broadcast. For confidential communication, use a named channel with a specific target. ### Changelog - `cdeaec3` - Introduced NAP-INC topic and channel communication. - `6461e4b` - Adopted unnumbered convention topics for napplet messages. - `f24a708` - Separated stable topic identity from per-message payload data. - `8782bb1` - Added runtime transposition from convention URI parameters to topic payload data. - `13fed4b` - Made sender attestation runtime-derived and carrier-neutral. - `6446441` - Made channel handles symmetric with target attachment, delivery ordering, and terminal notification. - `24ab958` - Replaced bare dTag peer identity with authenticated endpoint identifiers for all manifest kinds and adopted injected-domain availability. [Read specification on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) On this pageSpecification metadata Capability domain `inc` Repository state Merged Registry status Draft Document marker draft Declared dependencies None declared in the preamble Source revision `43cb3a4e8555` [View source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) --- [← All NAPs](https://napplet.run/protocol/) NAP-INTENT / PR #106 # Archetype Intent Dispatcher Open PRProposed amendment This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: supported** Convention normalization and dispatch have focused binding and reference-runtime integration tests. Implementation proposal: [merged ↗](https://github.com/napplet/naps/pull/91), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/fc121fc264615482143eda86125863d2e1f741a2/naps/NAP-INTENT.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/106/files). ## NAP-INTENT ### Archetype Intent Dispatcher `draft` **NAP ID:** NAP-INTENT **Domain:** `intent` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.intent`; domain presence signals availability. ### Description NAP-INTENT lets a napplet invoke another napplet by convention URI. The runtime derives the requested archetype and action, resolves an installed handler, accepts responsibility for delivery, and mediates the target lifecycle. The **source** is the invoking napplet. The **target** is the resolved handler. The source names a role and intent, never a target instance unless the user has explicitly authorized one. The developer-facing URI is `napplet:/[?params][#]`. Its queryless, fragment-free path is the stable convention identity. The runtime-provided binding derives the normalized `archetype`, `action`, `convention`, and `payload` fields before sending the wire request. The URI is authoritative. A successful invocation transfers delivery responsibility to the runtime. It does not assert that the target already received or handled the intent. The runtime may deliver to an existing target, start a target before closing the source, or close the source before starting the target. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `invoke` | convention URI (`text`), optional options (`IntentInvokeOptions`) | `IntentResult` | `intent.invoke` / `intent.invoke.result` | | `open` | convention URI whose intent is `open` (`text`), optional options (`IntentInvokeOptions`) | `IntentResult` | sugar over `invoke` | | `available` | archetype (`text`) | `IntentAvailability` | `intent.available` / `intent.available.result` | | `handlers` | none | list of `IntentAvailability` | `intent.handlers` / `intent.handlers.result` | | `onChanged` | handler for `IntentAvailability` | `Subscription` handle | `intent.changed` | | `onDelivery` | handler for `IntentDelivery` | `Subscription` handle | `intent.deliver` | #### Schemas `IntentBehavior` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `focus` | no | boolean | Request focus for the target surface. | | `reuse` | no | boolean | Permit reuse of an existing matching target. | These fields are hints. Runtime lifecycle and workspace policy remain authoritative. `IntentInvokeOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `payload` | no | any | Structured or non-text payload; cannot accompany URI query parameters. | | `handler` | no | text | `default`, `choose`, or an authorized catalog identifier; see Catalog identifiers. | | `handlerHint` | no | `IntentHandlerHint` | Explicit recommendation; cannot accompany a URI fragment. | | `behavior` | no | `IntentBehavior` | Lifecycle and focus hints. | `IntentHandlerHint` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `address` | yes | text | `35129::` coordinate decoded from the recommended napplet's address. | | `relays` | no | list of text | Optional discovery hints; not part of the napplet identity. | A napplet address is a [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) coordinate, `::`. The pubkey is exactly 64 lowercase hex digits; the non-empty `d` value is opaque and case-sensitive. Clients MUST NOT normalize `d` or identify a handler by `d` alone. `IntentHandlerHint` accepts kind `35129` only. Existing catalog entries MAY use other napplet manifest kinds; a hint does not change which manifest formats the runtime supports. `IntentRequest` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `archetype` | yes | text | Derived from the convention URI. | | `action` | yes | text | Derived from the convention URI intent. | | `convention` | yes | text | Stable, queryless, fragment-free convention identity. | | `payload` | no | any | Query-derived text map or explicit payload. | | `handler` | no | text | `default`, `choose`, or an authorized catalog identifier. | | `handlerHint` | no | `IntentHandlerHint` | Recommendation decoded from the URI fragment or supplied in options. | | `behavior` | no | `IntentBehavior` | Lifecycle and focus hints. | `IntentContract` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `convention` | yes | text | Stable, queryless, fragment-free convention identity. | | `params` | yes | list of text | Parameter names advertised by the manifest's `i` tag; empty when none are advertised. | `IntentCandidate` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Runtime-assigned catalog identifier for the napplet; see Catalog identifiers. | | `title` | no | text | Human-readable handler label. | | `actions` | yes | list of text | Actions derived from accepted conventions. | | `conventions` | yes | list of text | Stable convention identities. | | `contracts` | yes | list of `IntentContract` | Parsed manifest contracts. | | `isDefault` | no | boolean | Whether this candidate is the user's default. | `IntentAvailability` fields: | Field | Required | Type | | --- | --- | --- | | `archetype` | yes | text | | `available` | yes | boolean | | `candidates` | yes | list of `IntentCandidate` | | `hasDefault` | yes | boolean | `IntentResult` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ok` | yes | boolean | Whether the runtime accepted delivery responsibility. | | `archetype` | no | text | Normalized requested role. Required when `ok` is true. | | `action` | no | text | Normalized requested action. Required when `ok` is true. | | `convention` | no | text | Stable convention identity. Required when `ok` is true. | | `handler` | no | text | Resolved handler's catalog identifier. Required when `ok` is true. | | `error` | no | text | Pre-acceptance failure reason. Required when `ok` is false. | `IntentDelivery` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `sender` | yes | text | Runtime-attested source catalog identifier. | | `archetype` | yes | text | Normalized target role. | | `action` | yes | text | Normalized action. | | `convention` | yes | text | Stable convention identity. | | `payload` | no | any | Normalized convention payload. | No intent delivery identifier is exposed. The wire request `id` correlates only `intent.invoke` with its immediate `intent.invoke.result`. #### Convention URI normalization The binding MUST derive these request fields before sending `intent.invoke`: | Field | Source | | --- | --- | | `archetype`, `action` | URI segments `/`. | | `convention` | URI without query or fragment. | | `payload` | Percent-decoded `name=value` query fields, or `options.payload` for structured/non-text data. | | `handlerHint` | Bare [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) `naddr` fragment decoded as `IntentHandlerHint`, or `options.handlerHint`. | Query values MUST remain text; `+` is literal. The binding MUST reject malformed percent-encoding, duplicate decoded query names, invalid fragments, query plus `options.payload`, or fragment plus `options.handlerHint` before invocation. A fragment MUST satisfy the `IntentHandlerHint` address constraints. Only NAP-INTENT invocation accepts fragments; other convention-URI operations MUST reject them. A queryless URI MAY combine a fragment with `options.payload`. The runtime MUST reject a request unless `convention` is exactly the queryless, fragment-free `napplet:/` matching its `archetype` and `action` fields. **`invoke(uri, options?)`** — Normalizes the URI, resolves a handler, and asks the runtime to accept delivery responsibility. A successful result may precede target startup and delivery. **`open(uri, options?)`** — Convenience form of `invoke`. The URI intent MUST be `open`. **`available(archetype)`** — Returns installed candidates and their stable convention contracts. A candidate need not be running. **`handlers()`** — Returns availability for every archetype known to the runtime. **`onChanged(handler)`** — Receives availability changes after catalog or default-handler updates. **`onDelivery(handler)`** — Receives an `IntentDelivery` after the target is ready. The runtime supplies `sender`; the source napplet cannot set or override it. The runtime-provided binding MUST retain an incoming delivery until an `onDelivery` handler is registered. ### Manifest Catalog Contract The manifest schema and verification rules belong to [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303). Catalogs MUST recognize its napplet manifest kinds `5129` (snapshot), `15129` (root), and `35129` (named). Only named manifests carry a `d` tag. NAP-INTENT defines how their advertisements become dispatch contracts; it does not define another manifest format. A napplet advertises a role with `z` and each accepted convention with `i`: ``` ["z", ""] ["i", "napplet:/", "", ...] ``` For each valid `i` tag, the runtime MUST expose an `IntentContract` whose `convention` is the tag's second element and whose `params` are the remaining elements, in order. With no trailing elements, `params` is empty. The convention MUST be a stable, queryless, fragment-free identity. Its archetype segment MUST match a `z` value on the same manifest to be eligible for dispatch by that role. Its intent segment defines the action. Tag order is irrelevant; a runtime MUST NOT associate `i` with the nearest preceding `z` tag. A napplet repeats `z` for multiple roles and `i` for multiple accepted intents: ``` ["z", "feed"] ["i", "napplet:feed/open"] ["i", "napplet:feed/edit", "filters", "relays"] ``` Trailing `i` values advertise parameter names, not event-kind constraints, types, required fields, or parameter values. They MUST NOT be interpreted as `kind:` restrictions. An empty `params` list does not prohibit an explicit payload. Payload requirements and validation belong to the convention and the target. The runtime MUST NOT inspect payload content to infer an event kind or select a handler. A `z` tag alone does not declare an accepted intent; an `i` tag alone does not advertise its role. Missing or unusable advertisements MUST NOT invalidate an otherwise valid napplet manifest. They contribute no matching dispatch contract. Legacy combined `archetype` tags MUST NOT substitute for `z` and `i`. Runtimes MUST build `available()` and `handlers()` from these manifest tags. For each role, candidates MUST have at least one eligible contract, and their `contracts` MUST contain only contracts for that role. Runtimes MUST derive the candidate's `actions` and `conventions` from those contracts, removing duplicates. Handler resolution MUST match the requested stable convention by exact equality; it MUST NOT prefix-match, wildcard-match, or normalize that identity. Advertisements are routing hints, never capability grants. The runtime MUST apply the manifest capability requirements and its own policy before accepting delivery. Declaring a role or intent MUST NOT widen the domains exposed to the napplet. In the web projection, domain availability comes from the injected namespace as defined by [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303); neither catalog discovery nor invocation requires a `shell` domain or handshake. #### Catalog identifiers `IntentCandidate.id`, an explicit `handler`, a successful result's `handler`, and `IntentDelivery.sender` use the same runtime-assigned identifier space. Identifiers are opaque text: callers MUST preserve them exactly and MUST NOT interpret them as a `d` tag, event address, or running-instance identifier. `handlerHint.address` is a recommendation coordinate, not a catalog identifier. The runtime resolves a permitted recommendation to a verified catalog entry before selecting its identifier; callers MUST NOT substitute a hint coordinate for an explicit `handler`. The runtime MUST reserve `default` and `choose` for handler selection. Identifiers MUST distinguish publishers and manifest kinds. Named napplets are keyed by publisher, kind, and `d`; root napplets by publisher and kind; snapshots by signed event id. Updating a root or named manifest MUST preserve its catalog identifier. Distinct snapshots MUST have distinct identifiers. A bare `d` tag MUST NOT serve as catalog identity. Identifier encoding and persistence across runtime restarts are runtime policy; identifiers are not portable between runtimes. The runtime MUST also assign an identifier to the source, even when it has no dispatch advertisements. It MUST derive `sender` from the source's verified manifest bound to its authenticated endpoint. Catalog identity does not replace the web projection's artifact verification or endpoint binding. ### Wire Protocol `intent.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `intent.invoke` | napplet -> runtime | `id`, `request` | | `intent.invoke.result` | runtime -> napplet | `id`, `result` | | `intent.available` | napplet -> runtime | `id`, `archetype` | | `intent.available.result` | runtime -> napplet | `id`, `availability`, `error`? | | `intent.handlers` | napplet -> runtime | `id` | | `intent.handlers.result` | runtime -> napplet | `id`, `handlers`, `error`? | | `intent.changed` | runtime -> napplet | `availability` | | `intent.deliver` | runtime -> napplet | `delivery` | Key design notes: - Request/result pairs use `id` only for immediate correlation. - `intent.deliver` is a runtime push and has no request or delivery id. - The runtime-provided binding buffers `intent.deliver` when no `onDelivery` handler is registered yet. - Handler discovery uses stable, queryless convention identities. - Delivery uses the same `IntentDelivery` shape whether the target was already running or started for the invocation. - NAP-INTENT does not depend on NAP-INC. A runtime MAY use INC internally for a live target, but that is not visible to the target contract. #### Examples **Discover profile handlers:** For an installed manifest advertising `["z", "profile"]` and `["i", "napplet:profile/open", "pubkey"]`: ``` -> { "type": "intent.available", "id": "a1", "archetype": "profile" } <- { "type": "intent.available.result", "id": "a1", "availability": { "archetype": "profile", "available": true, "candidates": [{ "id": "catalog-profile-viewer", "title": "Profile Viewer", "actions": ["open"], "conventions": ["napplet:profile/open"], "contracts": [{ "convention": "napplet:profile/open", "params": ["pubkey"] }], "isDefault": true }], "hasDefault": true } } ``` **Invoke a convention URI:** ``` invoke("napplet:profile/open?pubkey=abc123...") ``` The runtime binding sends the normalized request: ``` -> { "type": "intent.invoke", "id": "i1", "request": { "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } } <- { "type": "intent.invoke.result", "id": "i1", "result": { "ok": true, "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "handler": "catalog-profile-viewer" } } ``` After acceptance, the runtime may leave the source open or close it. Once the target is ready, it receives: ``` <- { "type": "intent.deliver", "delivery": { "sender": "catalog-social-feed", "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } } ``` **Invoke with a structured payload:** ``` invoke("napplet:note/open", { "payload": { "target": { "type": "event", "id": "abc..." } } }) ``` **No handler installed:** ``` -> { "type": "intent.invoke", "id": "i2", "request": { "archetype": "emoji-list", "action": "open", "convention": "napplet:emoji-list/open" } } <- { "type": "intent.invoke.result", "id": "i2", "result": { "ok": false, "error": "no handler" } } ``` #### Error Handling The runtime MUST reject an invocation before acceptance when the convention URI or normalized request is invalid, no compatible handler exists, policy denies the request, or the user cancels handler selection. Common errors include `"invalid convention"`, `"no handler"`, `"unsupported convention"`, `"user cancelled"`, and `"invoke rejected"`. An `ok: true` result transfers responsibility to the runtime. Target startup, delivery timing, retries, terminal failure handling, and persistence across runtime restart are runtime policy. A post-acceptance failure is not reported as a second result to the source. ### Runtime Behavior - The runtime MUST derive the source `sender` from the authenticated endpoint. It MUST ignore or reject caller-supplied sender data. - The runtime MUST validate normalized `archetype`, `action`, and `convention` consistency before handler resolution. - The runtime MUST keep a user-overridable default per archetype. A napplet cannot set or change it. - The runtime SHOULD offer a chooser when `handler` is `choose`, or when several candidates exist and no default is set. - The runtime MUST resolve an installed handler using manifest contracts and the user's default-handler preference. - The runtime MUST respond to `intent.invoke` with the same request `id`. - An `ok: true` result MUST mean the runtime accepted delivery responsibility. - Before closing the source because of an invocation, the runtime MUST send its `intent.invoke.result`. - After acceptance, the runtime MUST retain the normalized delivery independently of the source until runtime policy determines delivery succeeded or failed terminally. - After acceptance, delivery MUST NOT depend on the source remaining alive. - The runtime MUST deliver `IntentDelivery` only after the target is ready. - The runtime MAY reuse an existing target, briefly overlap source and target, or close the source before starting the target. - Retry behavior, target replacement, overlap, terminal failure handling, and persistence across runtime restart are runtime policy. - `behavior` fields are hints. Runtime workspace and lifecycle policy remain authoritative. - The runtime MUST source `available()` and `handlers()` from verified installed [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) manifests, not only running instances. - The runtime MUST NOT let a caller address a handler instance unless the user explicitly authorized that handler. - The runtime MUST reject an unsupported convention before acceptance. - The runtime SHOULD emit `intent.changed` when catalog or default-handler state changes. #### Recommended handler resolution The shell MUST apply this precedence: 1. An explicit authorized `handler` catalog identifier or `handler: "choose"` selection. 2. The user's applicable default handler. 3. The recommended handler, if policy permits and it supports the requested archetype, action, and convention. 4. Normal installed-handler resolution or user choice. Absent `handler` and `handler: "default"` both use steps 2–4. An explicit selection MUST NOT silently fall back to the recommendation. An applicable handler supports the requested contract and is permitted by shell policy. Support MUST come from its verified catalog metadata, not from the hint itself. Convention matching uses exact stable identities. At step 3 the shell SHOULD consider a compatible installed recommendation. If it is not installed, the shell MAY resolve its signed event using the coordinate and relay hints and offer installation under its policy. The shell MUST verify the resolved event's signature and exact coordinate before considering it. It MUST apply its ordinary installation, artifact verification, and execution rules. Discovery does not authorize installation or execution. The hint MUST NOT alter the user's default. An `naddr` identifies a coordinate, not an artifact version. An unavailable, unsupported, declined, or policy-denied recommendation MUST fall back to step 4. A shell MAY ignore the recommendation under its policy. It need not perform discovery or offer installation. Relay hints are untrusted discovery inputs and MUST NOT bypass the shell's network policy. Discovery is shell-internal; it imposes no additional capability requirement on the caller. ### Security Considerations - Invocation may navigate, focus, start, or close napplet surfaces. The runtime SHOULD rate-limit or require a user gesture according to policy. - The runtime MUST derive `sender`; a napplet cannot impersonate another source. - Query transposition interprets URI syntax only. After normalization, payload semantics remain opaque to the runtime. - The target MUST validate `payload` against `convention` and treat `sender` as provenance, not proof that payload content is safe. - Handler selection is a trust boundary. Only installed manifest contracts, user defaults, or explicit user choice may select a target. - Default-handler state belongs to the user. A napplet MUST NOT change it. - Availability can reveal installed napplets. The runtime MAY redact candidate details according to policy. - Delivery MUST NOT leak payload or sender metadata to any napplet other than the resolved target. ### References - [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) — shareable address identifiers and relay hints. - [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) — addressable event coordinates and resolution. - [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) — napplet manifests, artifact verification, identity, domain availability, and normative web binding. - [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md) — existing napplet manifests. ### Implementations - (none yet) ### Changelog - `ad0847b` - Introduced archetype-based intent dispatch with action as data. - `6461e4b` - Adopted unnumbered convention identities for payload shapes. - `6c0d731` - Made convention URIs authoritative and delivery lifecycle-independent while preserving manifest-derived handler discovery. - `3dc945f` - Aligned manifest discovery with z/i advertisements and parameter names, supported all napplet manifest kinds with publisher-safe catalog identifiers, and adopted injected-domain availability. - `f89efbe` - Reconciled handler recommendation normalization and schemas with opaque catalog selection; kept fragments outside convention identity and payload. [Join the discussion · PR #106 ↗](https://github.com/napplet/naps/pull/106)[Read proposed changes ↗](https://github.com/napplet/naps/pull/106/files) On this pageSpecification metadata Capability domain `intent` Repository state Open PR Registry status Draft Document marker draft Declared dependencies None declared in the preamble Source revision `fc121fc26461` [View source on GitHub ↗](https://github.com/napplet/naps/pull/106/files) ## Also under this NAP - [Archetype Intent Dispatcher Merged →](https://napplet.run/protocol/nap-intent/) - [Archetype Intent Dispatcher Open PR · #128 →](https://napplet.run/protocol/nap-intent-pr-128/) --- [← All NAPs](https://napplet.run/protocol/) NAP-INTENT / PR #128 # Archetype Intent Dispatcher Open PRProposed amendment This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: supported** Convention normalization and dispatch have focused binding and reference-runtime integration tests. Implementation proposal: [merged ↗](https://github.com/napplet/naps/pull/91), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/74f9bf85ebb83b50b7b2ad8308a835d5f3113cd9/naps/NAP-INTENT.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/128/files). ## NAP-INTENT ### Archetype Intent Dispatcher `draft` **NAP ID:** NAP-INTENT **Domain:** `intent` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.intent`; domain presence signals availability. ### Description NAP-INTENT lets a napplet invoke another napplet by convention URI. The runtime derives the requested archetype and action, resolves an installed handler, accepts responsibility for delivery, and mediates the target lifecycle. The **source** is the invoking napplet. The **target** is the resolved handler. The source names a role and intent, never a target instance unless the user has explicitly authorized one. The developer-facing URI is `napplet:/[?params][#]`. Its queryless, fragment-free path is the stable convention identity. The runtime-provided binding derives the normalized `archetype`, `action`, `convention`, and `payload` fields before sending the wire request. The URI is authoritative. A successful invocation transfers delivery responsibility to the runtime. It does not assert that the target already received or handled the intent. The runtime may deliver to an existing target, start a target before closing the source, or close the source before starting the target. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `invoke` | convention URI (`text`), optional options (`IntentInvokeOptions`) | `IntentResult` | `intent.invoke` / `intent.invoke.result` | | `open` | convention URI whose intent is `open` (`text`), optional options (`IntentInvokeOptions`) | `IntentResult` | sugar over `invoke` | | `available` | archetype (`text`) | `IntentAvailability` | `intent.available` / `intent.available.result` | | `handlers` | none | list of `IntentAvailability` | `intent.handlers` / `intent.handlers.result` | | `onChanged` | handler for `IntentAvailability` | `Subscription` handle | `intent.changed` | | `onDelivery` | handler for `IntentDelivery` | `Subscription` handle | `intent.deliver` | #### Schemas `IntentBehavior` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `focus` | no | boolean | Request focus for the target surface. | | `reuse` | no | boolean | Permit reuse of an existing matching target. | These fields are hints. Runtime lifecycle and workspace policy remain authoritative. `IntentInvokeOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `payload` | no | any | Structured or non-text payload; cannot accompany URI query parameters. | | `handler` | no | text | `default`, `choose`, or an authorized catalog identifier; see Catalog identifiers. | | `handlerHint` | no | `IntentHandlerHint` | Explicit recommendation; cannot accompany a URI fragment. | | `behavior` | no | `IntentBehavior` | Lifecycle and focus hints. | `IntentHandlerHint` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `address` | yes | text | `35129::` coordinate decoded from the recommended napplet's address. | | `relays` | no | list of text | Optional discovery hints; not part of the napplet identity. | A napplet address is a [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) coordinate, `::`. The pubkey is exactly 64 lowercase hex digits; the non-empty `d` value is opaque and case-sensitive. Clients MUST NOT normalize `d` or identify a handler by `d` alone. `IntentHandlerHint` accepts kind `35129` only. Existing catalog entries MAY use other napplet manifest kinds; a hint does not change which manifest formats the runtime supports. `IntentRequest` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `archetype` | yes | text | Derived from the convention URI. | | `action` | yes | text | Derived from the convention URI intent. | | `convention` | yes | text | Stable, queryless, fragment-free convention identity. | | `payload` | no | any | Query-derived text map or explicit payload. | | `handler` | no | text | `default`, `choose`, or an authorized catalog identifier. | | `handlerHint` | no | `IntentHandlerHint` | Recommendation decoded from the URI fragment or supplied in options. | | `behavior` | no | `IntentBehavior` | Lifecycle and focus hints. | `IntentContract` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `convention` | yes | text | Stable, queryless, fragment-free convention identity. | | `params` | yes | list of text | Parameter names advertised by the manifest's `i` tag; empty when none are advertised. | `IntentCandidate` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Runtime-assigned catalog identifier for the napplet; see Catalog identifiers. | | `title` | no | text | Human-readable handler label. | | `actions` | yes | list of text | Actions derived from accepted conventions. | | `conventions` | yes | list of text | Stable convention identities. | | `contracts` | yes | list of `IntentContract` | Parsed manifest contracts. | | `isDefault` | no | boolean | Whether this candidate is the user's default. | `IntentAvailability` fields: | Field | Required | Type | | --- | --- | --- | | `archetype` | yes | text | | `available` | yes | boolean | | `candidates` | yes | list of `IntentCandidate` | | `hasDefault` | yes | boolean | `IntentResult` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ok` | yes | boolean | Whether the runtime accepted delivery responsibility. | | `archetype` | no | text | Normalized requested role. Required when `ok` is true. | | `action` | no | text | Normalized requested action. Required when `ok` is true. | | `convention` | no | text | Stable convention identity. Required when `ok` is true. | | `handler` | no | text | Resolved handler's catalog identifier. Required when `ok` is true. | | `error` | no | text | Pre-acceptance failure reason. Required when `ok` is false. | `IntentDelivery` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `sender` | yes | text | Runtime-attested source catalog identifier. | | `archetype` | yes | text | Normalized target role. | | `action` | yes | text | Normalized action. | | `convention` | yes | text | Stable convention identity. | | `payload` | no | any | Normalized convention payload. | No intent delivery identifier is exposed. The wire request `id` correlates only `intent.invoke` with its immediate `intent.invoke.result`. #### Convention URI normalization The binding MUST derive these request fields before sending `intent.invoke`: | Field | Source | | --- | --- | | `archetype`, `action` | URI segments `/`. | | `convention` | URI without query or fragment. | | `payload` | Percent-decoded `name=value` query fields, or `options.payload` for structured/non-text data. | | `handlerHint` | Bare [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) `naddr` fragment decoded as `IntentHandlerHint`, or `options.handlerHint`. | Query values MUST remain text; `+` is literal. The binding MUST reject malformed percent-encoding, duplicate decoded query names, invalid fragments, query plus `options.payload`, or fragment plus `options.handlerHint` before invocation. A fragment MUST satisfy the `IntentHandlerHint` address constraints. Only NAP-INTENT invocation accepts fragments; other convention-URI operations MUST reject them. A queryless URI MAY combine a fragment with `options.payload`. The runtime MUST reject a request unless `convention` is exactly the queryless, fragment-free `napplet:/` matching its `archetype` and `action` fields. **`invoke(uri, options?)`** — Normalizes the URI, resolves a handler, and asks the runtime to accept delivery responsibility. A successful result may precede target startup and delivery. **`open(uri, options?)`** — Convenience form of `invoke`. The URI intent MUST be `open`. **`available(archetype)`** — Returns installed candidates and their stable convention contracts. A candidate need not be running. **`handlers()`** — Returns availability for every archetype known to the runtime. **`onChanged(handler)`** — Receives availability changes after catalog or default-handler updates. **`onDelivery(handler)`** — Receives an `IntentDelivery` after the target is ready. The runtime supplies `sender`; the source napplet cannot set or override it. The runtime-provided binding MUST retain an incoming delivery until an `onDelivery` handler is registered. ### Manifest Catalog Contract The manifest schema and verification rules belong to [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303). Catalogs MUST recognize its napplet manifest kinds `5129` (snapshot), `15129` (root), and `35129` (named). Only named manifests carry a `d` tag. NAP-INTENT defines how their advertisements become dispatch contracts; it does not define another manifest format. A napplet advertises a role with `z` and each accepted convention with `i`: ``` ["z", ""] ["i", "napplet:/", "", ...] ``` For each valid `i` tag, the runtime MUST expose an `IntentContract` whose `convention` is the tag's second element and whose `params` are the remaining elements, in order. With no trailing elements, `params` is empty. The convention MUST be a stable, queryless, fragment-free identity. Its archetype segment MUST match a `z` value on the same manifest to be eligible for dispatch by that role. Its intent segment defines the action. Tag order is irrelevant; a runtime MUST NOT associate `i` with the nearest preceding `z` tag. A napplet repeats `z` for multiple roles and `i` for multiple accepted intents: ``` ["z", "feed"] ["i", "napplet:feed/open"] ["i", "napplet:feed/edit", "filters", "relays"] ``` Trailing `i` values advertise parameter names, not event-kind constraints, types, required fields, or parameter values. They MUST NOT be interpreted as `kind:` restrictions. An empty `params` list does not prohibit an explicit payload. Payload requirements and validation belong to the convention and the target. The runtime MUST NOT inspect payload content to infer an event kind or select a handler. A `z` tag alone does not declare an accepted intent; an `i` tag alone does not advertise its role. Missing or unusable advertisements MUST NOT invalidate an otherwise valid napplet manifest. They contribute no matching dispatch contract. Legacy combined `archetype` tags MUST NOT substitute for `z` and `i`. Runtimes MUST build `available()` and `handlers()` from these manifest tags. For each role, candidates MUST have at least one eligible contract, and their `contracts` MUST contain only contracts for that role. Runtimes MUST derive the candidate's `actions` and `conventions` from those contracts, removing duplicates. Handler resolution MUST match the requested stable convention by exact equality; it MUST NOT prefix-match, wildcard-match, or normalize that identity. Advertisements are routing hints, never capability grants. The runtime MUST apply the manifest capability requirements and its own policy before accepting delivery. Declaring a role or intent MUST NOT widen the domains exposed to the napplet. In the web projection, domain availability comes from the injected namespace as defined by [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303); neither catalog discovery nor invocation requires a `shell` domain or handshake. #### Catalog identifiers `IntentCandidate.id`, an explicit `handler`, a successful result's `handler`, and `IntentDelivery.sender` use the same runtime-assigned identifier space. Identifiers are opaque text: callers MUST preserve them exactly and MUST NOT interpret them as a `d` tag, event address, or running-instance identifier. `handlerHint.address` is a recommendation coordinate, not a catalog identifier. The runtime resolves a permitted recommendation to a verified catalog entry before selecting its identifier; callers MUST NOT substitute a hint coordinate for an explicit `handler`. The runtime MUST reserve `default` and `choose` for handler selection. Identifiers MUST distinguish publishers and manifest kinds. Named napplets are keyed by publisher, kind, and `d`; root napplets by publisher and kind; snapshots by signed event id. Updating a root or named manifest MUST preserve its catalog identifier. Distinct snapshots MUST have distinct identifiers. A bare `d` tag MUST NOT serve as catalog identity. Identifier encoding and persistence across runtime restarts are runtime policy; identifiers are not portable between runtimes. The runtime MUST also assign an identifier to the source, even when it has no dispatch advertisements. It MUST derive `sender` from the source's verified manifest bound to its authenticated endpoint. Catalog identity does not replace the web projection's artifact verification or endpoint binding. ### Wire Protocol `intent.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `intent.invoke` | napplet -> runtime | `id`, `request` | | `intent.invoke.result` | runtime -> napplet | `id`, `result` | | `intent.available` | napplet -> runtime | `id`, `archetype` | | `intent.available.result` | runtime -> napplet | `id`, `availability`, `error`? | | `intent.handlers` | napplet -> runtime | `id` | | `intent.handlers.result` | runtime -> napplet | `id`, `handlers`, `error`? | | `intent.changed` | runtime -> napplet | `availability` | | `intent.deliver` | runtime -> napplet | `delivery` | Key design notes: - Request/result pairs use `id` only for immediate correlation. - `intent.deliver` is a runtime push and has no request or delivery id. - The runtime-provided binding buffers `intent.deliver` when no `onDelivery` handler is registered yet. - Handler discovery uses stable, queryless convention identities. - Delivery uses the same `IntentDelivery` shape whether the target was already running or started for the invocation. - NAP-INTENT does not depend on NAP-INC. A runtime MAY use INC internally for a live target, but that is not visible to the target contract. #### Examples **Discover profile handlers:** For an installed manifest advertising `["z", "profile"]` and `["i", "napplet:profile/open", "pubkey"]`: ``` -> { "type": "intent.available", "id": "a1", "archetype": "profile" } <- { "type": "intent.available.result", "id": "a1", "availability": { "archetype": "profile", "available": true, "candidates": [{ "id": "catalog-profile-viewer", "title": "Profile Viewer", "actions": ["open"], "conventions": ["napplet:profile/open"], "contracts": [{ "convention": "napplet:profile/open", "params": ["pubkey"] }], "isDefault": true }], "hasDefault": true } } ``` **Invoke a convention URI:** ``` invoke("napplet:profile/open?pubkey=abc123...") ``` The runtime binding sends the normalized request: ``` -> { "type": "intent.invoke", "id": "i1", "request": { "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } } <- { "type": "intent.invoke.result", "id": "i1", "result": { "ok": true, "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "handler": "catalog-profile-viewer" } } ``` After acceptance, the runtime may leave the source open or close it. Once the target is ready, it receives: ``` <- { "type": "intent.deliver", "delivery": { "sender": "catalog-social-feed", "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } } ``` **Invoke with a structured payload:** ``` invoke("napplet:note/open", { "payload": { "target": { "type": "event", "id": "abc..." } } }) ``` **No handler installed:** ``` -> { "type": "intent.invoke", "id": "i2", "request": { "archetype": "emoji-list", "action": "open", "convention": "napplet:emoji-list/open" } } <- { "type": "intent.invoke.result", "id": "i2", "result": { "ok": false, "error": "no handler" } } ``` #### Error Handling The runtime MUST reject an invocation before acceptance when the convention URI or normalized request is invalid, no compatible handler exists, policy denies the request, or the user cancels handler selection. Common errors include `"invalid convention"`, `"no handler"`, `"unsupported convention"`, `"user cancelled"`, and `"invoke rejected"`. An `ok: true` result transfers responsibility to the runtime. Target startup, delivery timing, retries, terminal failure handling, and persistence across runtime restart are runtime policy. A post-acceptance failure is not reported as a second result to the source. ### Runtime Behavior - The runtime MUST derive the source `sender` from the authenticated endpoint. It MUST ignore or reject caller-supplied sender data. - The runtime MUST validate normalized `archetype`, `action`, and `convention` consistency before handler resolution. - The runtime MUST keep a user-overridable default per archetype. A napplet cannot set or change it. - The runtime SHOULD offer a chooser when `handler` is `choose`, or when several candidates exist and no default is set. - The runtime MUST resolve an installed handler using manifest contracts and the user's default-handler preference. - The runtime MUST respond to `intent.invoke` with the same request `id`. - An `ok: true` result MUST mean the runtime accepted delivery responsibility. - Before closing the source because of an invocation, the runtime MUST send its `intent.invoke.result`. - After acceptance, the runtime MUST retain the normalized delivery independently of the source until runtime policy determines delivery succeeded or failed terminally. - After acceptance, delivery MUST NOT depend on the source remaining alive. - The runtime MUST deliver `IntentDelivery` only after the target is ready. - The runtime MAY reuse an existing target, briefly overlap source and target, or close the source before starting the target. - Retry behavior, target replacement, overlap, terminal failure handling, and persistence across runtime restart are runtime policy. Delivery to a selected instance MUST keep its endpoint; see below. - `behavior` fields are hints. Runtime workspace and lifecycle policy remain authoritative. - The runtime MUST source `available()` and `handlers()` from verified installed [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) manifests, not only running instances. - The runtime MUST NOT let a caller address a handler instance unless the user explicitly authorized that handler. - The runtime MUST reject an unsupported convention before acceptance. - The runtime SHOULD emit `intent.changed` when catalog or default-handler state changes. #### Delivery to a selected instance Under user action or shell policy, the shell MAY ask the runtime to replay an invocation to one particular running instance. A history revisit is the motivating case. This adds no operation, wire message, or field: the target receives an ordinary `IntentDelivery`. The shell is not a source. It has no catalog identifier and MUST NOT author intent content. A selected-instance delivery MUST replay an invocation the target itself produced, such as a location it reported under another domain's contract. Its `sender` MUST be the target's own catalog identifier. The runtime MUST NOT fabricate, reserve, or null a sender for this purpose. For this delivery, the runtime MUST: 1. Bind the request to the selected instance's authenticated endpoint. The selection is shell-internal; it is not a catalog identifier supplied through `handler`. 2. Confirm that the instance is alive and has the `intent` domain. 3. Check the convention against that instance's verified manifest using the eligible-contract and exact-identity rules of ordinary delivery. 4. Deliver the replayed `archetype`, `action`, `convention`, and `payload` unchanged, with no added payload fields. The runtime MUST NOT run a chooser, consult a default, start another instance, or fall back to another endpoint. If the endpoint terminates or cannot accept the convention, the request fails. A retry MUST NOT transfer it to a replacement instance. A target MUST handle a delivery whose `sender` is its own catalog identifier like any other delivery. It validates the payload as usual. Delivery does not prove that the target handled the request, and no receipt is exposed. A napplet cannot request selected-instance delivery. Its `intent.invoke` always uses catalog resolution. The runtime MUST ignore or reject a napplet-supplied instance selector, sender, or payload field offered as one. | Case | Required result | | --- | --- | | Shell selects one of two instances of the same manifest | Deliver only to the selected endpoint. | | Replay of the target's own invocation | Ordinary `IntentDelivery`; `sender` is the target's catalog identifier. | | Shell-authored content or a non-napplet sender | Reject; the shell is not a source. | | Selected instance does not advertise the convention | Fail; do not choose another handler. | | User default changes after selection | Keep the selected endpoint. | | Selected endpoint terminates before delivery | Fail; do not restart or substitute an instance. | | No `onDelivery` handler registered yet | Apply the ordinary buffering rule. | | Napplet supplies an instance selector or sender | Ignore or reject; use ordinary catalog resolution. | #### Recommended handler resolution The shell MUST apply this precedence: 1. An explicit authorized `handler` catalog identifier or `handler: "choose"` selection. 2. The user's applicable default handler. 3. The recommended handler, if policy permits and it supports the requested archetype, action, and convention. 4. Normal installed-handler resolution or user choice. Absent `handler` and `handler: "default"` both use steps 2–4. An explicit selection MUST NOT silently fall back to the recommendation. An applicable handler supports the requested contract and is permitted by shell policy. Support MUST come from its verified catalog metadata, not from the hint itself. Convention matching uses exact stable identities. At step 3 the shell SHOULD consider a compatible installed recommendation. If it is not installed, the shell MAY resolve its signed event using the coordinate and relay hints and offer installation under its policy. The shell MUST verify the resolved event's signature and exact coordinate before considering it. It MUST apply its ordinary installation, artifact verification, and execution rules. Discovery does not authorize installation or execution. The hint MUST NOT alter the user's default. An `naddr` identifies a coordinate, not an artifact version. An unavailable, unsupported, declined, or policy-denied recommendation MUST fall back to step 4. A shell MAY ignore the recommendation under its policy. It need not perform discovery or offer installation. Relay hints are untrusted discovery inputs and MUST NOT bypass the shell's network policy. Discovery is shell-internal; it imposes no additional capability requirement on the caller. ### Security Considerations - Invocation may navigate, focus, start, or close napplet surfaces. The runtime SHOULD rate-limit or require a user gesture according to policy. - The runtime MUST derive `sender`; a napplet cannot impersonate another source. - Query transposition interprets URI syntax only. After normalization, payload semantics remain opaque to the runtime. - The target MUST validate `payload` against `convention` and treat `sender` as provenance, not proof that payload content is safe. - Handler selection is a trust boundary. Only installed manifest contracts, user defaults, or explicit user choice may select a target. A selected-instance delivery stays bound to its authenticated endpoint and replays only that target's own invocation. - Default-handler state belongs to the user. A napplet MUST NOT change it. - Availability can reveal installed napplets. The runtime MAY redact candidate details according to policy. - Delivery MUST NOT leak payload or sender metadata to any napplet other than the resolved target. ### References - [NAP-NAVIGATION proposal](https://github.com/napplet/naps/pull/126) — replays reported locations through selected-instance delivery; intent does not depend on navigation. - [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) — shareable address identifiers and relay hints. - [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) — addressable event coordinates and resolution. - [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) — napplet manifests, artifact verification, identity, domain availability, and normative web binding. - [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md) — existing napplet manifests. ### Implementations - (none yet) ### Changelog - `ad0847b` - Introduced archetype-based intent dispatch with action as data. - `6461e4b` - Adopted unnumbered convention identities for payload shapes. - `6c0d731` - Made convention URIs authoritative and delivery lifecycle-independent while preserving manifest-derived handler discovery. - `3dc945f` - Aligned manifest discovery with z/i advertisements and parameter names, supported all napplet manifest kinds with publisher-safe catalog identifiers, and adopted injected-domain availability. - `f89efbe` - Reconciled handler recommendation normalization and schemas with opaque catalog selection; kept fragments outside convention identity and payload. - `6e4af53` - Added explicit shell provenance and endpoint-bound intent delivery to an existing instance, without catalog fallback or a new dispatch operation. - `4f8ebe7` - Added target-scoped delivery identifiers for report correlation, with stable retry identity, lifetime scope, and no source-facing completion promise. - `df8496a` - Replaced delivery identifiers and the null shell sender with additive selected-instance delivery: the shell authors no content, a replay carries the target's own catalog identifier, the endpoint never falls back, and the wire format is unchanged. [Join the discussion · PR #128 ↗](https://github.com/napplet/naps/pull/128)[Read proposed changes ↗](https://github.com/napplet/naps/pull/128/files) On this pageSpecification metadata Capability domain `intent` Repository state Open PR Registry status Draft Document marker draft Declared dependencies None declared in the preamble Source revision `74f9bf85ebb8` [View source on GitHub ↗](https://github.com/napplet/naps/pull/128/files) ## Also under this NAP - [Archetype Intent Dispatcher Merged →](https://napplet.run/protocol/nap-intent/) - [Archetype Intent Dispatcher Open PR · #106 →](https://napplet.run/protocol/nap-intent-pr-106/) --- [← All NAPs](https://napplet.run/protocol/) NAP-INTENT / Merged specification # Archetype Intent Dispatcher Merged **Shipped implementation: supported** Convention normalization and dispatch have focused binding and reference-runtime integration tests. Implementation proposal: [merged ↗](https://github.com/napplet/naps/pull/91), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/naps/NAP-INTENT.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). ## NAP-INTENT ### Archetype Intent Dispatcher `draft` **NAP ID:** NAP-INTENT **Domain:** `intent` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.intent`; domain presence signals availability. ### Description NAP-INTENT lets a napplet invoke another napplet by convention URI. The runtime derives the requested archetype and action, resolves an installed handler, accepts responsibility for delivery, and mediates the target lifecycle. The **source** is the invoking napplet. The **target** is the resolved handler. The source names a role and intent, never a target instance unless the user has explicitly authorized one. The developer-facing URI is `napplet:/[...?params]`. Its queryless path is the stable convention identity. The runtime-provided binding derives the normalized `archetype`, `action`, `convention`, and `payload` fields before sending the wire request. The URI is authoritative. A successful invocation transfers delivery responsibility to the runtime. It does not assert that the target already received or handled the intent. The runtime may deliver to an existing target, start a target before closing the source, or close the source before starting the target. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `invoke` | convention URI (`text`), optional options (`IntentInvokeOptions`) | `IntentResult` | `intent.invoke` / `intent.invoke.result` | | `open` | convention URI whose intent is `open` (`text`), optional options (`IntentInvokeOptions`) | `IntentResult` | sugar over `invoke` | | `available` | archetype (`text`) | `IntentAvailability` | `intent.available` / `intent.available.result` | | `handlers` | none | list of `IntentAvailability` | `intent.handlers` / `intent.handlers.result` | | `onChanged` | handler for `IntentAvailability` | `Subscription` handle | `intent.changed` | | `onDelivery` | handler for `IntentDelivery` | `Subscription` handle | `intent.deliver` | #### Schemas `IntentBehavior` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `focus` | no | boolean | Request focus for the target surface. | | `reuse` | no | boolean | Permit reuse of an existing matching target. | These fields are hints. Runtime lifecycle and workspace policy remain authoritative. `IntentInvokeOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `payload` | no | any | Structured or non-text convention payload. | | `handler` | no | text | `default`, `choose`, or an authorized catalog identifier. | | `behavior` | no | `IntentBehavior` | Lifecycle and focus hints. | `IntentInvokeOptions` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `payload` | no | any | Structured or non-text payload; cannot accompany URI query parameters. | | `handler` | no | text | `default`, `choose`, or an authorized napplet address. | | `handlerHint` | no | `IntentHandlerHint` | Explicit recommendation; cannot accompany a URI fragment. | | `behavior` | no | `IntentBehavior` | Window/focus hints. | `IntentHandlerHint` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `address` | yes | text | `35129::` coordinate decoded from the recommended napplet's address. | | `relays` | no | list of text | Optional discovery hints; not part of the napplet identity. | A napplet address is a [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) coordinate, `::`. The pubkey is exactly 64 lowercase hex digits; the non-empty `d` value is opaque and case-sensitive. Clients MUST NOT normalize `d` or identify a handler by `d` alone. `IntentHandlerHint` accepts kind `35129` only. Existing catalog entries MAY use other napplet manifest kinds; a hint does not change which manifest formats the runtime supports. `IntentRequest` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `archetype` | yes | text | Derived from the convention URI. | | `action` | yes | text | Derived from the convention URI intent. | | `convention` | yes | text | Stable, queryless convention identity. | | `payload` | no | any | Query-derived text map or explicit payload. | | `handler` | no | text | `default`, `choose`, or an authorized catalog identifier. | | `behavior` | no | `IntentBehavior` | Lifecycle and focus hints. | `IntentContract` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `convention` | yes | text | Stable, queryless convention identity. | | `params` | yes | list of text | Parameter names advertised by the manifest's `i` tag; empty when none are advertised. | `IntentCandidate` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `id` | yes | text | Runtime-assigned catalog identifier for the napplet; see Catalog identifiers. | | `title` | no | text | Human-readable handler label. | | `actions` | yes | list of text | Actions derived from accepted conventions. | | `conventions` | yes | list of text | Stable convention identities. | | `contracts` | yes | list of `IntentContract` | Parsed manifest contracts. | | `isDefault` | no | boolean | Whether this candidate is the user's default. | `IntentAvailability` fields: | Field | Required | Type | | --- | --- | --- | | `archetype` | yes | text | | `available` | yes | boolean | | `candidates` | yes | list of `IntentCandidate` | | `hasDefault` | yes | boolean | `IntentResult` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `ok` | yes | boolean | Whether the runtime accepted delivery responsibility. | | `archetype` | no | text | Normalized requested role. Required when `ok` is true. | | `action` | no | text | Normalized requested action. Required when `ok` is true. | | `convention` | no | text | Stable convention identity. Required when `ok` is true. | | `handler` | no | text | Resolved handler's catalog identifier. Required when `ok` is true. | | `error` | no | text | Pre-acceptance failure reason. Required when `ok` is false. | `IntentDelivery` fields: | Field | Required | Type | Notes | | --- | --- | --- | --- | | `sender` | yes | text | Runtime-attested source catalog identifier. | | `archetype` | yes | text | Normalized target role. | | `action` | yes | text | Normalized action. | | `convention` | yes | text | Stable convention identity. | | `payload` | no | any | Normalized convention payload. | No intent delivery identifier is exposed. The wire request `id` correlates only `intent.invoke` with its immediate `intent.invoke.result`. #### Convention URI normalization The runtime-provided binding MUST normalize a convention URI before sending `intent.invoke`: 1. The first URI component after `napplet:` becomes `archetype`. 2. The path component after `/` becomes `action`. 3. The queryless URI becomes `convention`. 4. Each unique, percent-decoded `name=value` query pair becomes a text payload field. The binding MUST NOT coerce query values to boolean, number, or null. A `+` is a literal plus sign, not a space. A URI with a fragment, malformed percent-encoding, a repeated query name, or an explicit payload alongside query parameters MUST be rejected before invocation. Structured or non-text data MUST use `options.payload` with a queryless URI. The runtime MUST reject a normalized wire request when: - `convention` contains a query or fragment; - the convention archetype differs from `request.archetype`; or - the convention intent differs from `request.action`. **`invoke(uri, options?)`** — Normalizes the URI, resolves a handler, and asks the runtime to accept delivery responsibility. A successful result may precede target startup and delivery. **`open(uri, options?)`** — Convenience form of `invoke`. The URI intent MUST be `open`. **`available(archetype)`** — Returns installed candidates and their stable convention contracts. A candidate need not be running. **`handlers()`** — Returns availability for every archetype known to the runtime. **`onChanged(handler)`** — Receives availability changes after catalog or default-handler updates. **`onDelivery(handler)`** — Receives an `IntentDelivery` after the target is ready. The runtime supplies `sender`; the source napplet cannot set or override it. The runtime-provided binding MUST retain an incoming delivery until an `onDelivery` handler is registered. ### Manifest Catalog Contract The manifest schema and verification rules belong to [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303). Catalogs MUST recognize its napplet manifest kinds `5129` (snapshot), `15129` (root), and `35129` (named). Only named manifests carry a `d` tag. NAP-INTENT defines how their advertisements become dispatch contracts; it does not define another manifest format. A napplet advertises a role with `z` and each accepted convention with `i`: ``` ["z", ""] ["i", "napplet:/", "", ...] ``` For each valid `i` tag, the runtime MUST expose an `IntentContract` whose `convention` is the tag's second element and whose `params` are the remaining elements, in order. With no trailing elements, `params` is empty. The convention MUST be a stable, queryless, fragment-free identity. Its archetype segment MUST match a `z` value on the same manifest to be eligible for dispatch by that role. Its intent segment defines the action. Tag order is irrelevant; a runtime MUST NOT associate `i` with the nearest preceding `z` tag. A napplet repeats `z` for multiple roles and `i` for multiple accepted intents: ``` ["z", "feed"] ["i", "napplet:feed/open"] ["i", "napplet:feed/edit", "filters", "relays"] ``` Trailing `i` values advertise parameter names, not event-kind constraints, types, required fields, or parameter values. They MUST NOT be interpreted as `kind:` restrictions. An empty `params` list does not prohibit an explicit payload. Payload requirements and validation belong to the convention and the target. The runtime MUST NOT inspect payload content to infer an event kind or select a handler. A `z` tag alone does not declare an accepted intent; an `i` tag alone does not advertise its role. Missing or unusable advertisements MUST NOT invalidate an otherwise valid napplet manifest. They contribute no matching dispatch contract. Legacy combined `archetype` tags MUST NOT substitute for `z` and `i`. Runtimes MUST build `available()` and `handlers()` from these manifest tags. For each role, candidates MUST have at least one eligible contract, and their `contracts` MUST contain only contracts for that role. Runtimes MUST derive the candidate's `actions` and `conventions` from those contracts, removing duplicates. Handler resolution MUST match the requested stable convention by exact equality; it MUST NOT prefix-match, wildcard-match, or normalize that identity. Advertisements are routing hints, never capability grants. The runtime MUST apply the manifest capability requirements and its own policy before accepting delivery. Declaring a role or intent MUST NOT widen the domains exposed to the napplet. In the web projection, domain availability comes from the injected namespace as defined by [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303); neither catalog discovery nor invocation requires a `shell` domain or handshake. #### Catalog identifiers `IntentCandidate.id`, an explicit `handler`, a successful result's `handler`, and `IntentDelivery.sender` use the same runtime-assigned identifier space. Identifiers are opaque text: callers MUST preserve them exactly and MUST NOT interpret them as a `d` tag, event address, or running-instance identifier. The runtime MUST reserve `default` and `choose` for handler selection. Identifiers MUST distinguish publishers and manifest kinds. Named napplets are keyed by publisher, kind, and `d`; root napplets by publisher and kind; snapshots by signed event id. Updating a root or named manifest MUST preserve its catalog identifier. Distinct snapshots MUST have distinct identifiers. A bare `d` tag MUST NOT serve as catalog identity. Identifier encoding and persistence across runtime restarts are runtime policy; identifiers are not portable between runtimes. The runtime MUST also assign an identifier to the source, even when it has no dispatch advertisements. It MUST derive `sender` from the source's verified manifest bound to its authenticated endpoint. Catalog identity does not replace the web projection's artifact verification or endpoint binding. ### Wire Protocol `intent.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `intent.invoke` | napplet -> runtime | `id`, `request` | | `intent.invoke.result` | runtime -> napplet | `id`, `result` | | `intent.available` | napplet -> runtime | `id`, `archetype` | | `intent.available.result` | runtime -> napplet | `id`, `availability`, `error`? | | `intent.handlers` | napplet -> runtime | `id` | | `intent.handlers.result` | runtime -> napplet | `id`, `handlers`, `error`? | | `intent.changed` | runtime -> napplet | `availability` | | `intent.deliver` | runtime -> napplet | `delivery` | Key design notes: - Request/result pairs use `id` only for immediate correlation. - `intent.deliver` is a runtime push and has no request or delivery id. - The runtime-provided binding buffers `intent.deliver` when no `onDelivery` handler is registered yet. - Handler discovery uses stable, queryless convention identities. - Delivery uses the same `IntentDelivery` shape whether the target was already running or started for the invocation. - NAP-INTENT does not depend on NAP-INC. A runtime MAY use INC internally for a live target, but that is not visible to the target contract. #### Examples **Discover profile handlers:** For an installed manifest advertising `["z", "profile"]` and `["i", "napplet:profile/open", "pubkey"]`: ``` -> { "type": "intent.available", "id": "a1", "archetype": "profile" } <- { "type": "intent.available.result", "id": "a1", "availability": { "archetype": "profile", "available": true, "candidates": [{ "id": "catalog-profile-viewer", "title": "Profile Viewer", "actions": ["open"], "conventions": ["napplet:profile/open"], "contracts": [{ "convention": "napplet:profile/open", "params": ["pubkey"] }], "isDefault": true }], "hasDefault": true } } ``` **Invoke a convention URI:** ``` invoke("napplet:profile/open?pubkey=abc123...") ``` The runtime binding sends the normalized request: ``` -> { "type": "intent.invoke", "id": "i1", "request": { "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } } <- { "type": "intent.invoke.result", "id": "i1", "result": { "ok": true, "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "handler": "catalog-profile-viewer" } } ``` After acceptance, the runtime may leave the source open or close it. Once the target is ready, it receives: ``` <- { "type": "intent.deliver", "delivery": { "sender": "catalog-social-feed", "archetype": "profile", "action": "open", "convention": "napplet:profile/open", "payload": { "pubkey": "abc123..." } } } ``` **Invoke with a structured payload:** ``` invoke("napplet:note/open", { "payload": { "target": { "type": "event", "id": "abc..." } } }) ``` **No handler installed:** ``` -> { "type": "intent.invoke", "id": "i2", "request": { "archetype": "emoji-list", "action": "open", "convention": "napplet:emoji-list/open" } } <- { "type": "intent.invoke.result", "id": "i2", "result": { "ok": false, "error": "no handler" } } ``` #### Error Handling The runtime MUST reject an invocation before acceptance when the convention URI or normalized request is invalid, no compatible handler exists, policy denies the request, or the user cancels handler selection. Common errors include `"invalid convention"`, `"no handler"`, `"unsupported convention"`, `"user cancelled"`, and `"invoke rejected"`. An `ok: true` result transfers responsibility to the runtime. Target startup, delivery timing, retries, terminal failure handling, and persistence across runtime restart are runtime policy. A post-acceptance failure is not reported as a second result to the source. ### Runtime Behavior - The runtime MUST derive the source `sender` from the authenticated endpoint. It MUST ignore or reject caller-supplied sender data. - The runtime MUST validate normalized `archetype`, `action`, and `convention` consistency before handler resolution. - The runtime MUST keep a user-overridable default per archetype. A napplet cannot set or change it. - The runtime SHOULD offer a chooser when `handler` is `choose`, or when several candidates exist and no default is set. - The runtime MUST resolve an installed handler using manifest contracts and the user's default-handler preference. - The runtime MUST respond to `intent.invoke` with the same request `id`. - An `ok: true` result MUST mean the runtime accepted delivery responsibility. - Before closing the source because of an invocation, the runtime MUST send its `intent.invoke.result`. - After acceptance, the runtime MUST retain the normalized delivery independently of the source until runtime policy determines delivery succeeded or failed terminally. - After acceptance, delivery MUST NOT depend on the source remaining alive. - The runtime MUST deliver `IntentDelivery` only after the target is ready. - The runtime MAY reuse an existing target, briefly overlap source and target, or close the source before starting the target. - Retry behavior, target replacement, overlap, terminal failure handling, and persistence across runtime restart are runtime policy. - `behavior` fields are hints. Runtime workspace and lifecycle policy remain authoritative. - The runtime MUST source `available()` and `handlers()` from verified installed [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) manifests, not only running instances. - The runtime MUST NOT let a caller address a handler instance unless the user explicitly authorized that handler. - The runtime MUST reject an unsupported convention before acceptance. - The runtime SHOULD emit `intent.changed` when catalog or default-handler state changes. #### Recommended handler resolution The shell MUST apply this precedence: 1. An explicit authorized `handler` address or `handler: "choose"` selection. 2. The user's applicable default handler. 3. The recommended handler, if policy permits and it supports the requested archetype, action, and convention. 4. Normal installed-handler resolution or user choice. Absent `handler` and `handler: "default"` both use steps 2–4. An explicit selection MUST NOT silently fall back to the recommendation. An applicable handler supports the requested contract and is permitted by shell policy. Support MUST come from its verified catalog metadata, not from the hint itself. Convention matching uses exact stable identities. At step 3 the shell SHOULD consider a compatible installed recommendation. If it is not installed, the shell MAY resolve its signed event using the coordinate and relay hints and offer installation under its policy. The shell MUST verify the resolved event's signature and exact coordinate before considering it. It MUST apply its ordinary installation, artifact verification, and execution rules. Discovery does not authorize installation or execution. The hint MUST NOT alter the user's default. An `naddr` identifies a coordinate, not an artifact version. An unavailable, unsupported, declined, or policy-denied recommendation MUST fall back to step 4. A shell MAY ignore the recommendation under its policy. It need not perform discovery or offer installation. Relay hints are untrusted discovery inputs and MUST NOT bypass the shell's network policy. Discovery is shell-internal; it imposes no additional capability requirement on the caller. ### Security Considerations - Invocation may navigate, focus, start, or close napplet surfaces. The runtime SHOULD rate-limit or require a user gesture according to policy. - The runtime MUST derive `sender`; a napplet cannot impersonate another source. - Query transposition interprets URI syntax only. After normalization, payload semantics remain opaque to the runtime. - The target MUST validate `payload` against `convention` and treat `sender` as provenance, not proof that payload content is safe. - Handler selection is a trust boundary. Only installed manifest contracts, user defaults, or explicit user choice may select a target. - Default-handler state belongs to the user. A napplet MUST NOT change it. - Availability can reveal installed napplets. The runtime MAY redact candidate details according to policy. - Delivery MUST NOT leak payload or sender metadata to any napplet other than the resolved target. ### References - [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) — napplet manifests, artifact verification, identity, domain availability, and normative web binding. ### References - [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) — shareable address identifiers and relay hints. - [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) — addressable event coordinates and resolution. - [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) — normative web binding. - [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md) — existing napplet manifests. ### Implementations - (none yet) ### Changelog - `ad0847b` - Introduced archetype-based intent dispatch with action as data. - `6461e4b` - Adopted unnumbered convention identities for payload shapes. - `6c0d731` - Made convention URIs authoritative and delivery lifecycle-independent while preserving manifest-derived handler discovery. - `3dc945f` - Aligned manifest discovery with z/i advertisements and parameter names, supported all napplet manifest kinds with publisher-safe catalog identifiers, and adopted injected-domain availability. [Read specification on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) On this pageSpecification metadata Capability domain `intent` Repository state Merged Registry status Draft Document marker draft Declared dependencies None declared in the preamble Source revision `43cb3a4e8555` [View source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) ## Also under this NAP - [Archetype Intent Dispatcher Open PR · #106 →](https://napplet.run/protocol/nap-intent-pr-106/) - [Archetype Intent Dispatcher Open PR · #128 →](https://napplet.run/protocol/nap-intent-pr-128/) --- [← All NAPs](https://napplet.run/protocol/) NAP-KEYS / PR #9 # Keyboard Forwarding and Action Keybindings Open PR This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it. **Shipped implementation: experimental** Keyboard binding ships, but dedicated domain regression coverage is limited; retain explicit evaluation opt-in. Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/9), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change. [Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html) Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/8869a4cf77b023f4b049f47c546910a0280dc1a6/naps/NAP-KEYS.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/9/files). ## NAP-KEYS ### Keyboard Forwarding and Action Keybindings `draft` **NAP ID:** NAP-KEYS **Domain:** `keys` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.keys`; domain presence signals availability. ### Description NAP-KEYS provides bidirectional keyboard interaction between napplets and the shell. Sandboxed iframes capture keyboard events and prevent them from reaching the parent window, so shell-level hotkeys (workspace switching, command palette, etc.) do not work when a napplet has focus. NAP-KEYS solves this with two mechanisms: keyboard forwarding (napplet sends keystrokes to the shell) and action registration (napplet declares named actions that the shell can bind to keys). When a napplet registers an action and the shell binds it to a key, the shell pushes binding updates to the napplet. The napplet then suppresses forwarding for bound keys and triggers the action locally. This eliminates dual-fire (both napplet and shell reacting to the same keystroke) and gives the shell full control over the keymap. ### API Surface | Operation | Parameters | Result | Wire | | --- | --- | --- | --- | | `registerAction` | `action` (`Action`) | `RegisterResult` | `keys.registerAction` / `keys.registerAction.result` | | `unregisterAction` | `actionId` (`tstr`) | none | `keys.unregisterAction` | | `forward` | keyboard event fields | none | `keys.forward` | | `onAction` | `actionId` (`tstr`), local callback | `Subscription` handle | local helper; not a wire message | #### Schemas ```cddl Action = { id: tstr, ; unique action identifier, e.g. "editor.save" label: tstr, ; human-readable label for shell keybinding UI ? defaultKey: tstr, ; suggested binding hint, e.g. "Ctrl+S" } RegisterResult = { actionId: tstr, ? binding: tstr, ; key combo assigned by the shell, absent when unbound } ``` **`registerAction(action)`** -- Declares a named action that the shell can bind to a key. The `defaultKey` is a hint only — the shell decides the actual binding. Returns the assigned binding if one was made immediately. The shell MAY defer binding until the user configures it. **`unregisterAction(actionId)`** -- Removes a previously registered action. The shell removes any binding for it and updates the napplet's suppress list. **`forward(event)`** -- Sends a keystroke to the shell. The shim calls this automatically for keydown events that are not in the suppress list. Napplets do not call this directly under normal use. **`onAction(actionId, callback)`** -- Registers a local handler for when a bound key is pressed. This is NOT a wire message — it is SDK convenience that listens for the key locally and invokes the callback. Returns a Subscription with `close()` to stop listening. ### Wire Protocol `keys.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`). | Type | Direction | Payload fields | | --- | --- | --- | | `keys.forward` | napplet -> shell | `key`, `code`, `ctrl`, `alt`, `shift`, `meta` | | `keys.registerAction` | napplet -> shell | `id` (correlation), `action` (Action object) | | `keys.registerAction.result` | shell -> napplet | `id`, `actionId`, `binding`? | | `keys.unregisterAction` | napplet -> shell | `actionId` | | `keys.bindings` | shell -> napplet | `bindings` (array of `{ actionId, key }`) | | `keys.action` | shell -> napplet | `actionId` | Key design notes: - `keys.forward` is fire-and-forget (no `id`). High frequency — one per keystroke. - `keys.registerAction` / `keys.registerAction.result` use `id` for correlation. - `keys.unregisterAction` is fire-and-forget. - `keys.bindings` is a shell-initiated push. Sent whenever bindings change (initial load, user rebind, action unregister). Contains the complete binding list — not a diff. - `keys.action` is a shell-initiated push when the shell wants to trigger an action in the napplet (e.g., the user pressed a globally-bound key while a different napplet has focus, but the action belongs to this napplet). #### Key Combo Format Key combinations use the format `Modifier+Key` where modifiers are `Ctrl`, `Alt`, `Shift`, `Meta` and the key is the `KeyboardEvent.key` value. Examples: `Ctrl+S`, `Alt+Shift+P`, `F5`, `Escape`. Shells MUST normalize combos to alphabetical modifier order: `Alt+Ctrl+Shift+Meta+Key`. Napplets SHOULD use this order in `defaultKey` hints. #### Forwarding Fields The `keys.forward` message carries the essential `KeyboardEvent` fields: | Field | Type | Source | | --- | --- | --- | | `key` | string | `KeyboardEvent.key` — character intent (e.g., `"s"`, `"Enter"`, `"ArrowDown"`) | | `code` | string | `KeyboardEvent.code` — physical key position (e.g., `"KeyS"`, `"Enter"`, `"ArrowDown"`) | | `ctrl` | boolean | `KeyboardEvent.ctrlKey` | | `alt` | boolean | `KeyboardEvent.altKey` | | `shift` | boolean | `KeyboardEvent.shiftKey` | | `meta` | boolean | `KeyboardEvent.metaKey` | Both `key` and `code` are included. `key` is used for command shortcut matching (intent-based, respects keyboard layout). `code` is available for shells that need position-based matching (e.g., WASD game controls). The shell decides which field to match against. #### Smart Forwarding The napplet shim maintains a local suppress list derived from `keys.bindings` messages. On each keydown event: 1. If the event target is a text input (``, `
0 / 280
`; function requireElement(selector: string): T { const element = document.querySelector(selector); if (!element) { throw new Error(`Missing required element: ${selector}`); } return element; } const pubkey = requireElement('#pubkey'); const draftStatus = requireElement('#draft-status'); const publishStatus = requireElement('#publish-status'); const form = requireElement('#composer'); const note = requireElement('#note'); const count = requireElement('#count'); const publishButton = requireElement('#publish'); function hasDomain(domain: RequiredDomain): boolean { return Boolean((window as NappletWindow).napplet?.[domain]); } function shortKey(value: string): string { return value.length > 16 ? `${value.slice(0, 8)}...${value.slice(-8)}` : value; } function updateCount(): void { count.textContent = `${note.value.length} / ${note.maxLength}`; } function setBusy(busy: boolean): void { publishButton.disabled = busy; publishButton.textContent = busy ? 'Publishing...' : 'Publish note'; } async function loadIdentity(): Promise { if (!hasDomain('identity')) { pubkey.textContent = 'identity unavailable'; return; } const current = await identity.getPublicKey(); pubkey.textContent = current ? shortKey(current) : 'signed out'; const subscription = identity.onChanged((next) => { pubkey.textContent = next ? shortKey(next) : 'signed out'; }); window.addEventListener('pagehide', () => subscription.close(), { once: true }); } async function loadDraft(): Promise { if (!hasDomain('storage')) { draftStatus.textContent = 'storage unavailable'; return; } const saved = await storage.instance.getItem(draftKey); if (saved) { note.value = saved; updateCount(); draftStatus.textContent = 'restored from shell storage'; } else { draftStatus.textContent = 'empty'; } } let saveTimer: number | undefined; function queueDraftSave(): void { updateCount(); if (!hasDomain('storage')) { draftStatus.textContent = 'storage unavailable'; return; } if (saveTimer !== undefined) { window.clearTimeout(saveTimer); } saveTimer = window.setTimeout(() => { void storage.instance .setItem(draftKey, note.value) .then(() => { draftStatus.textContent = note.value ? 'saved' : 'empty'; }) .catch((error: unknown) => { draftStatus.textContent = error instanceof Error ? error.message : 'save failed'; }); }, 250); } async function publishNote(): Promise { const content = note.value.trim(); if (!content) { publishStatus.textContent = 'Write something first'; return; } if (!hasDomain('outbox')) { publishStatus.textContent = 'outbox unavailable'; return; } setBusy(true); publishStatus.textContent = 'Waiting for shell confirmation...'; try { const result = await outbox.publish( { kind: 1, content, tags: [], created_at: Math.floor(Date.now() / 1000), }, { toOutbox: true }, ); if (!result.ok) { throw new Error(result.error ?? 'publish failed'); } note.value = ''; updateCount(); if (hasDomain('storage')) { await storage.instance.removeItem(draftKey); draftStatus.textContent = 'cleared after publish'; } publishStatus.textContent = `published ${shortKey(result.eventId ?? result.event?.id ?? '')}`; } catch (error) { publishStatus.textContent = error instanceof Error ? error.message : 'publish failed'; } finally { setBusy(false); } } note.addEventListener('input', queueDraftSave); form.addEventListener('submit', (event) => { event.preventDefault(); void publishNote(); }); updateCount(); void loadIdentity().catch((error: unknown) => { pubkey.textContent = error instanceof Error ? error.message : 'identity failed'; }); void loadDraft().catch((error: unknown) => { draftStatus.textContent = error instanceof Error ? error.message : 'draft load failed'; }); ``` What changed from the broad starter: - No napplet-side `@napplet/shim` import. - No shell capability-probing namespace. - No direct relay read path. - No app-owned persistence. - No signing or relay fanout code. ## 6. Replace the styles Replace `src/styles.css` with compact app styles: ```css :root { color: #f7f4ef; background: #17181c; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at top left, rgb(25 92 124 / 42%), transparent 30rem), linear-gradient(135deg, #17181c 0%, #22262d 52%, #1d2d2c 100%); } button, textarea { font: inherit; } .shell { width: min(100%, 760px); margin: 0 auto; padding: 32px; } .eyebrow, .label { color: #9fd4c9; font-size: 0.78rem; font-weight: 700; letter-spacing: 0; text-transform: uppercase; } h1 { margin: 6px 0; font-size: clamp(2rem, 7vw, 4.5rem); line-height: 0.95; } .lede { max-width: 36rem; margin: 0 0 24px; color: #d6d0c5; } .status-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0 0 18px; } .status-grid > div { min-width: 0; padding: 14px; border: 1px solid rgb(255 255 255 / 14%); border-radius: 8px; background: rgb(255 255 255 / 8%); } .status-grid strong { display: block; margin-top: 6px; overflow-wrap: anywhere; } .composer { display: grid; gap: 12px; } label { font-weight: 700; } textarea { width: 100%; min-height: 220px; resize: vertical; border: 1px solid rgb(255 255 255 / 18%); border-radius: 8px; padding: 16px; color: #f7f4ef; background: rgb(0 0 0 / 34%); } textarea:focus { outline: 2px solid #9fd4c9; outline-offset: 2px; } .actions { display: flex; align-items: center; justify-content: space-between; gap: 16px; } #count { color: #d6d0c5; } button { min-height: 44px; border: 0; border-radius: 8px; padding: 0 18px; color: #17211f; background: #9fd4c9; font-weight: 800; cursor: pointer; } button:disabled { cursor: wait; opacity: 0.64; } @media (max-width: 680px) { .shell { padding: 20px; } .status-grid { grid-template-columns: 1fr; } .actions { align-items: stretch; flex-direction: column; } } ``` ## 7. Update the project docs Replace the generated README's feature list with the app boundary: ```md # Note Drafts A small NIP-5D napplet for drafting and publishing kind `1` Nostr notes. The app uses: - `identity` to show the shell-user pubkey. - `storage` to autosave one draft under shell-managed storage scoping. - `outbox` to ask the shell to sign and publish. It does not access private keys, `window.nostr`, browser storage, relay sockets, or direct network APIs. ``` Keep the generated `docs/` context files. They are useful reminders for future edits, but the canonical protocol source remains the living NIP-5D and NAP text linked from those docs. ## 8. Verify the generated-project path Run the checks the boilerplate already provides: ```bash pnpm type-check pnpm build pnpm test:conformance ``` Inspect the generated signed manifest tags: ```bash grep -n '"d"\\|"requires"' dist/.nip5a-manifest.json ``` Expected manifest tags include: ```json ["d", "my-napplet"] ["requires", "identity"] ["requires", "outbox"] ["requires", "storage"] ``` The deploy preview must show the CLI-owned `notedrafts` d-tag and the title, description, and archetype from `.napplet/config.yaml`. Open the app in your target shell or Paja runtime and verify: 1. The runtime user status changes from "Checking identity..." to a pubkey or "signed out". 2. A typed draft survives iframe reload. 3. Publishing asks the shell to sign and fan out the note. 4. Disabling `storage` or `outbox` shows a status message instead of crashing. That proves the boilerplate substrate stayed intact while the app boundary became a focused Note Drafts napplet. Preview the CLI-owned deployment plan before publishing: ```bash napplet deploy --dry-run napplet deploy ``` --- # Tutorial: build a Note Drafts napplet with an AI agent and the napplet skills This tutorial builds the Note Drafts napplet with a coding agent instead of typing every file yourself. It uses the [`napplet-*` agent skills](https://napplet.run/docs/guide/agent-skills.md), the authoring guidance that teaches agents the current napplet package surface, the boilerplate-first project shape, the sandbox boundary, the applet UI contract, and the verification checklist. Use this path when you want the agent to do the implementation work, while you keep control of scope, review, and release evidence. Protocol references the agent must defer to: - [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303), the proposed web projection: sandboxed iframe, `postMessage` envelope, and runtime-injected domains - [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md), the manifest and artifact-hash model - [NAPs](https://github.com/napplet/naps), the capability-domain specs for `identity`, `storage`, and `outbox` - [NAP-INC PR #89 at `4593ce9`](https://github.com/napplet/naps/pull/89/commits/4593ce9e301ce098fd3dad64206fcd6f144fa7af), [governance/web PR #90 at `896c32c`](https://github.com/napplet/naps/pull/90/commits/896c32c92deee68dc4d10fc1132b62df20cccb6f), and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md), the exact draft heads adopted for convention URI and intent semantics ## 1. Create and initialize the project Start from the maintained scaffold and record deployment metadata before the agent edits application code: ```bash napplet create note-drafts cd note-drafts napplet init --name notedrafts --title "Note Drafts" \ --description "Draft and publish short Nostr notes from a sandboxed napplet." \ --archetype napplet:note/open ``` ## 2. Install the napplet skills for your agent The skills live in the napplet repository and install with the open [skills CLI](https://skills.sh), which detects your coding agents and places each skill where that agent reads it: ```bash npx skills add napplet/web ``` Useful variants: ```bash npx skills add napplet/web -a claude-code # one agent npx skills add napplet/web -g # global, all projects npx skills add napplet/web --skill napplet-make # a single skill ``` Install the full set for this tutorial; `napplet-make` routes the work through the others. What this teaches: the skill file is not a protocol spec. It is a build guide that tells the agent to check NIP-5D and NAPs, preserve the `napplet create` scaffold and `napplet init` metadata, use shipped `@napplet/*` package exports, and stop instead of inventing missing protocol surface. Before implementation, the agent should inspect the project state and available tools. An empty directory, a maintained boilerplate, an initialized napplet, a boilerplate-based brownfield app, and an unrelated brownfield app require different paths. It should also check whether `napplet` and Kehto/Paja are installed rather than assuming either binary exists. The configured queryless convention emits `['z', 'note']` and `['i', 'napplet:note/open']`. The agent must not infer a kind or payload schema from payload content. Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). ## 3. Give the agent a small product prompt Paste a prompt like this into your coding agent: ```text Build a Note Drafts napplet in this directory using the installed napplet-make skill. The app should let a user write one short Nostr note, autosave the draft, and publish it through the host shell. The project is already scaffolded and its deployment metadata is in .napplet/config.yaml. Follow the skills end to end and report the changed files plus verification evidence. ``` This prompt is intentionally short. The installed `napplet-make` skill is responsible for routing the work through `napplet-design`, `napplet-ui`, `napplet-build`, and `napplet-test`, preserving the `napplet create` scaffold, choosing the shell-mediated domains, keeping the UI compact and usable at any frame size, preserving the generated scripts, and refusing direct browser or signing authority. For this app, the skills should infer: - `identity` reads the shell-user pubkey and listens for changes. - `storage` autosaves and clears the draft under shell-managed storage. - `outbox` publishes the note with shell signing and fanout. - `requires` contains only the domains the app directly calls: `identity`, `storage`, and `outbox`. - If a needed API is missing from current packages or specs, the agent should stop and flag that package/spec gap instead of inventing a local protocol. ## 4. Review the agent's first diff Before running the app, inspect the files the agent changed: ```bash git status --short git diff -- package.json vite.config.ts index.html src/main.ts src/styles.css README.md ``` The diff should look like this: - `package.json` preserves the generated scripts and uses current `@napplet/sdk`, `@napplet/vite-plugin`, and `@napplet/conformance-cli` versions. - `.napplet/config.yaml` still owns `notedrafts`, title, description, and the queryless `napplet:note/open` archetype convention. - `vite.config.ts` declares `requires: ['identity', 'storage', 'outbox']` and does not become a second source of deployment metadata. - `src/main.ts` imports `identity`, `storage`, and `outbox` from `@napplet/sdk`. - Draft persistence goes through `storage.instance`. - Publishing goes through `outbox.publish` with an unsigned kind `1` event template. - The code checks for absent hard domains only to show user-facing errors; it does not invent a shell discovery API. Run a quick forbidden-surface scan. You should not need to paste this list into the first prompt; it is here so you can audit the result: ```bash rg "window\\.nostr|localStorage|sessionStorage|indexedDB|fetch\\(|WebSocket|EventSource|shell\\.ready|shell\\.supports|discoverServices" src ``` No matches is the expected result. If there are matches, ask the agent to repair them before continuing. ## 5. Use a repair prompt when needed If the first pass keeps starter-demo code, broad requirements, or shell bypasses, use a focused repair prompt: ```text The first pass drifted from the installed napplet skills. Re-run the napplet-make workflow (napplet-ui, napplet-build, napplet-test), repair any boundary, layout, or verification failures, and keep the Note Drafts product scope unchanged. ``` Keep repair prompts narrow. Do not ask the agent to redesign the whole app after you already have a working structure. If the same boundary detail must be added to every user prompt, treat that as a skill bug and update the relevant `skills/napplet-*/SKILL.md` instead of growing the prompt. ## 6. Verify the artifact, not just the source Run the same checks you would run by hand: ```bash pnpm install pnpm type-check pnpm build pnpm test:conformance ``` Then inspect the generated signed manifest tags: ```bash grep -n '"d"\\|"requires"' dist/.nip5a-manifest.json ``` Expected manifest tags include: ```json ["d", "notedrafts"] ["requires", "identity"] ["requires", "outbox"] ["requires", "storage"] ``` The manifest check matters because the shell reads the signed capability declaration. A source file can look correct while generated manifest tags remain stale. ## 7. Run a shell smoke test Use the shell/runtime you target for local testing. Start the project through Paja, not as a raw Vite page: ```bash napplet paja -- pnpm vite --host 127.0.0.1 ``` Report the URL printed by Paja. The underlying Vite URL is only an asset server and is not a valid napplet preview. If Paja is unavailable, report that manual runtime verification is pending instead of linking to Vite. In Paja or another compatible runtime: 1. Load the built or dev Note Drafts napplet through the shell, not as a raw Vite page. 2. Confirm identity renders as a short pubkey or signed-out state. 3. Type a draft and reload the iframe; the draft should restore through `storage`. 4. Publish a note; the shell should own signing and relay fanout. 5. Disable `storage` or `outbox` and reload; the UI should show a clear status message. Do not treat a raw browser preview as proof. The app only proves the napplet boundary when a shell injects the required NAP domains. The visual implementation should also use NAP-THEME when available. Apply `theme.colors.background` to `html`, `body`, and the app root, apply `theme.colors.text` and `theme.colors.primary` to the design tokens, and update the full surface from `themeOnChanged`. A dark component palette on a white page is not themed; test both dark and light backgrounds. ## 8. Ask the agent for a completion report Use one final prompt: ```text Summarize the Note Drafts napplet build. Include: - changed files - NAP domains used and why - forbidden surfaces scanned - commands that passed with exact output summaries - any manual shell smoke evidence - any untested gaps ``` The useful output is not "done"; it is evidence. Keep the report with the project or PR so future edits can tell which claims were verified. ## What the skills are buying you The skills do not make the agent authoritative. They make the default path better: - Preserve the `napplet create` scaffold and `napplet init` deployment metadata instead of recreating build plumbing. - Use current shipped `@napplet/sdk` helpers instead of imagined APIs. - Prefer `outbox` for social reads/publishes instead of direct relay ownership. - Keep browser storage, direct network, private keys, and external runtime assets out of napplet code. - Run conformance before claiming completion. You still review the diff, run the checks, and compare any protocol-facing claim against the living NIP-5D and NAP documents. --- # Tutorial: build a Note Drafts napplet from scratch This tutorial builds a small Nostr note composer that runs as a napplet in Kehto/Paja. The app starts with a runtime check, then grows one capability at a time: 1. Render the app frame and inspect runtime domain presence. 2. Read the current shell-user pubkey through read-only `identity`. 3. Autosave a draft through shell-scoped `storage`. 4. Publish a kind `1` note through `outbox`. The example is intentionally a note composer because composing is common Nostr UX, but it still teaches the napplet boundary. The napplet never sees a private key, never opens a relay socket, never writes browser storage, and never chooses write relays. The shell does those jobs. Protocol references used here: - [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303), the proposed web projection: sandboxed iframe, `postMessage` envelope, and runtime-injected domains - [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md), the manifest and artifact-hash model - [NAPs](https://github.com/napplet/naps), the capability-domain specs for `identity`, `storage`, and `outbox` - [NAP-INC PR #89 at `4593ce9`](https://github.com/napplet/naps/pull/89/commits/4593ce9e301ce098fd3dad64206fcd6f144fa7af), [governance/web PR #90 at `896c32c`](https://github.com/napplet/naps/pull/90/commits/896c32c92deee68dc4d10fc1132b62df20cccb6f), and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md), the exact draft heads adopted for convention URI and intent semantics ## 1. Create the project manually and initialize deployment This is the advanced manual-wiring path. New projects should normally use `napplet create`; this tutorial starts empty so each build file is visible, but still uses `napplet init` as the deployment metadata authority. ```bash mkdir note-drafts cd note-drafts napplet init --name notedrafts --title "Note Drafts" \ --description "Draft and publish short Nostr notes from a sandboxed napplet." \ --archetype napplet:note/open pnpm init pnpm add @napplet/sdk@^0.32.0 pnpm add -D @napplet/vite-plugin@^0.16.0 @napplet/conformance-cli@^0.6.0 @kehto/cli@^0.2.11 typescript@^5.9.3 vite@^6.4.5 ``` Replace the generated `package.json` with this: ```json { "name": "note-drafts", "version": "0.0.0", "private": true, "type": "module", "scripts": { "dev": "kehto paja --config kehto.dev.json -- pnpm vite --host 127.0.0.1", "build": "vite build", "type-check": "tsc --noEmit", "test:conformance": "pnpm build && napplet-conformance ./dist", "verify": "pnpm type-check && pnpm test:conformance" }, "dependencies": { "@napplet/sdk": "^0.32.0" }, "devDependencies": { "@kehto/cli": "^0.2.11", "@napplet/conformance-cli": "^0.6.0", "@napplet/vite-plugin": "^0.16.0", "typescript": "^5.9.3", "vite": "^6.4.5" } } ``` What this teaches: napplet application code uses `@napplet/sdk`; local runtime testing uses Kehto/Paja; conformance tests the built artifact, not source files. This tutorial declares the queryless convention `napplet:note/open`, so its manifest tag is `['z', 'note']` and `['i', 'napplet:note/open']`. The runtime never infers a kind or payload schema from payload content. If the app later uses INC `emit` or intent `invoke/open`, those two bindings may accept a queried convention URI and transpose its unique decoded pairs into a shallow text payload before sending the stable queryless identity. Subscriptions, manifest discovery, and handler resolution still use exact equality on the queryless identity. Intent invocation uses an explicit request object and its result reports whether a handler accepted the dispatch. ## 2. Configure TypeScript and the manifest plugin Add strict TypeScript: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"], "strict": true, "verbatimModuleSyntax": true, "types": ["vite/client"], "skipLibCheck": true }, "include": ["src", "vite.config.ts"] } ``` Now add `vite.config.ts`: ```ts import { defineConfig } from 'vite'; import { nip5aManifest } from '@napplet/vite-plugin'; export default defineConfig({ build: { modulePreload: false, }, plugins: [ nip5aManifest({ nappletType: 'notedrafts', title: 'Note Drafts', description: 'Draft and publish short Nostr notes from a sandboxed napplet.', requires: ['identity', 'storage', 'outbox'], artifactMode: 'single-file', }), ], }); ``` The important fields are: - `build.modulePreload: false`: keeps Vite from injecting a preload helper that calls `fetch`. - `nappletType`: the short named-site `d` tag for this napplet. - `requires`: hard domains the shell must provide for the full app. - `artifactMode: 'single-file'`: folds the built JS and CSS into one `dist/index.html`. What this teaches: `requires` is declarative compatibility metadata. Runtime code still checks domain presence so local toggles and smaller shells fail gracefully. ## 3. Add the HTML entry The source HTML is ordinary Vite HTML: ```html Note Drafts
``` Kehto/Paja will load this into a runtime iframe during development. The production build will inline the app script and CSS into `dist/index.html`. ## 4. Start with the napplet boundary Create `src/main.ts` with the imports, domain names, and app container: ```ts import { identity, outbox, storage } from '@napplet/sdk'; import './styles.css'; type RequiredDomain = 'identity' | 'storage' | 'outbox'; type NappletWindow = Window & { napplet?: Partial>; }; const draftKey = 'note-draft:v1'; const app = document.querySelector('#app'); if (!app) { throw new Error('Missing #app container'); } ``` This first step names the only runtime domains the app will use. There is no relay pool, signer object, `localStorage`, or `window.nostr`. What this teaches: a napplet is untrusted app code. It asks the runtime for capabilities through NAP domains. ## 5. Render a status-first UI Add the app markup and typed DOM lookups: ```ts app.innerHTML = `

Napplet tutorial

Note Drafts

Autosave a short note, then ask the shell to sign and publish it.

Runtime user Checking identity...
Draft Not loaded yet
Publish Idle
0 / 280
`; function requireElement(selector: string): T { const element = document.querySelector(selector); if (!element) { throw new Error(`Missing required element: ${selector}`); } return element; } const pubkey = requireElement('#pubkey'); const draftStatus = requireElement('#draft-status'); const publishStatus = requireElement('#publish-status'); const form = requireElement('#composer'); const note = requireElement('#note'); const count = requireElement('#count'); const publishButton = requireElement('#publish'); ``` Status text comes before the feature logic on purpose. A napplet should tell the user when a shell capability is absent instead of failing silently. ## 6. Check domain presence before calling SDK methods Add small helpers: ```ts function hasDomain(domain: RequiredDomain): boolean { return Boolean((window as NappletWindow).napplet?.[domain]); } function shortKey(value: string): string { return value.length > 16 ? `${value.slice(0, 8)}...${value.slice(-8)}` : value; } function setDraftStatus(message: string): void { draftStatus.textContent = message; } function setPublishStatus(message: string): void { publishStatus.textContent = message; } function updateCount(): void { count.textContent = `${note.value.length} / ${note.maxLength}`; } function setBusy(busy: boolean): void { publishButton.disabled = busy; publishButton.textContent = busy ? 'Publishing...' : 'Publish note'; } ``` The SDK methods throw clear errors if a domain is unavailable. The UI still checks first because a good napplet can explain missing runtime support to a person. ## 7. Read the shell-user identity Add identity loading: ```ts async function loadIdentity(): Promise { if (!hasDomain('identity')) { pubkey.textContent = 'identity unavailable'; return; } const current = await identity.getPublicKey(); pubkey.textContent = current ? shortKey(current) : 'signed out'; const subscription = identity.onChanged((next) => { pubkey.textContent = next ? shortKey(next) : 'signed out'; }); window.addEventListener('pagehide', () => subscription.close(), { once: true }); } ``` `identity` is read-only. It tells the napplet which shell-user is connected, but it does not sign, encrypt, or expose keys. The app takes a startup snapshot and then listens for shell-pushed changes. ## 8. Autosave through shell storage Add draft loading and debounced saving: ```ts async function loadDraft(): Promise { if (!hasDomain('storage')) { setDraftStatus('storage unavailable'); return; } const saved = await storage.instance.getItem(draftKey); if (saved) { note.value = saved; updateCount(); setDraftStatus('restored from shell storage'); } else { setDraftStatus('empty'); } } let saveTimer: number | undefined; function queueDraftSave(): void { updateCount(); if (!hasDomain('storage')) { setDraftStatus('storage unavailable'); return; } if (saveTimer !== undefined) { window.clearTimeout(saveTimer); } saveTimer = window.setTimeout(() => { void storage.instance .setItem(draftKey, note.value) .then(() => setDraftStatus(note.value ? 'saved' : 'empty')) .catch((error: unknown) => setDraftStatus(error instanceof Error ? error.message : 'save failed')); }, 250); } ``` This replaces browser storage. `storage.instance` is scoped by the shell to this napplet instance, so another napplet cannot read this draft. ## 9. Publish through outbox Add publishing: ```ts async function publishNote(): Promise { const content = note.value.trim(); if (!content) { setPublishStatus('Write something first'); return; } if (!hasDomain('outbox')) { setPublishStatus('outbox unavailable'); return; } setBusy(true); setPublishStatus('Waiting for shell confirmation...'); try { const result = await outbox.publish( { kind: 1, content, tags: [], created_at: Math.floor(Date.now() / 1000), }, { toOutbox: true }, ); if (!result.ok) { throw new Error(result.error ?? 'publish failed'); } note.value = ''; updateCount(); if (hasDomain('storage')) { await storage.instance.removeItem(draftKey); setDraftStatus('cleared after publish'); } setPublishStatus(`published ${shortKey(result.eventId ?? result.event?.id ?? '')}`); } catch (error) { setPublishStatus(error instanceof Error ? error.message : 'publish failed'); } finally { setBusy(false); } } ``` The napplet supplies an unsigned event template. The shell owns signing, relay selection, fanout, and policy. `toOutbox: true` asks the shell to include the user's write relays. ## 10. Wire the feature together Add the event listeners and startup calls: ```ts note.addEventListener('input', queueDraftSave); form.addEventListener('submit', (event) => { event.preventDefault(); void publishNote(); }); updateCount(); void loadIdentity().catch((error: unknown) => { pubkey.textContent = error instanceof Error ? error.message : 'identity failed'; }); void loadDraft().catch((error: unknown) => { setDraftStatus(error instanceof Error ? error.message : 'draft load failed'); }); ``` At this point `src/main.ts` is complete. The final behavior is layered, not magic: runtime capability check, identity snapshot, storage-backed draft, and outbox publish. ## 11. Add styles Create `src/styles.css`: ```css :root { color: #f7f4ef; background: #17181c; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at top left, rgb(25 92 124 / 42%), transparent 30rem), linear-gradient(135deg, #17181c 0%, #22262d 52%, #1d2d2c 100%); } button, textarea { font: inherit; } .shell { width: min(100%, 760px); margin: 0 auto; padding: 32px; } .masthead { margin: 0 0 24px; } .eyebrow, .label { color: #9fd4c9; font-size: 0.78rem; font-weight: 700; letter-spacing: 0; text-transform: uppercase; } h1 { margin: 6px 0; font-size: clamp(2rem, 7vw, 4.5rem); line-height: 0.95; } .lede { max-width: 36rem; margin: 0; color: #d6d0c5; } .status-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0 0 18px; } .status-grid > div { min-width: 0; padding: 14px; border: 1px solid rgb(255 255 255 / 14%); border-radius: 8px; background: rgb(255 255 255 / 8%); } .status-grid strong { display: block; margin-top: 6px; overflow-wrap: anywhere; } .composer { display: grid; gap: 12px; } label { font-weight: 700; } textarea { width: 100%; min-height: 220px; resize: vertical; border: 1px solid rgb(255 255 255 / 18%); border-radius: 8px; padding: 16px; color: #f7f4ef; background: rgb(0 0 0 / 34%); } textarea:focus { outline: 2px solid #9fd4c9; outline-offset: 2px; } .actions { display: flex; align-items: center; justify-content: space-between; gap: 16px; } #count { color: #d6d0c5; } button { min-height: 44px; border: 0; border-radius: 8px; padding: 0 18px; color: #17211f; background: #9fd4c9; font-weight: 800; cursor: pointer; } button:disabled { cursor: wait; opacity: 0.64; } @media (max-width: 680px) { .shell { padding: 20px; } .status-grid { grid-template-columns: 1fr; } .actions { align-items: stretch; flex-direction: column; } } ``` Run a first local check: ```bash pnpm type-check pnpm build ``` ## 12. Configure Napplet CLI and Paja Add a Napplet CLI config for discovery, conformance, and later deploy dry-runs: ```yaml version: 1 sourceDir: . relays: - wss://relay.damus.io blossomServers: - https://cdn.hzrd149.com defaultTarget: named named: - notedrafts conformance: command: napplet-conformance paja: command: kehto ``` Add a Paja simulation config: ```json { "targetUrl": "http://127.0.0.1:5173", "simulation": { "identity": { "mode": "fixed", "pubkey": "4444444444444444444444444444444444444444444444444444444444444444" }, "capabilities": { "domains": { "identity": true, "storage": true, "outbox": true } }, "theme": { "mode": "dark" } } } ``` Run the app: ```bash pnpm dev ``` Open the Paja runtime URL printed by the command, not the raw Vite URL. In Paja: 1. Confirm `identity`, `storage`, and `outbox` are enabled in Interfaces. 2. Grant storage read/write and outbox write in ACL when Paja asks. 3. Type a draft, reload the target iframe, and confirm the draft is restored. 4. Publish the note and confirm Paja asks before signing/publishing. Then toggle off `storage` or `outbox` in Interfaces and reload. The app should show a status message rather than throwing. ## 13. Verify the artifact Run: ```bash pnpm verify ``` That command type-checks the app, builds the single-file artifact, and runs `napplet-conformance` against `dist/index.html`. Inspect the generated signed manifest tags: ```bash pnpm build grep -n '"d"\\|"requires"' dist/.nip5a-manifest.json ``` Expected manifest tags include: ```json ["d", "notedrafts"] ["requires", "identity"] ["requires", "outbox"] ["requires", "storage"] ``` The built `dist/index.html` is the file you test and publish, and its signed manifest carries the protocol metadata. Do not point conformance or deploy tooling at source `index.html`; it still contains the Vite `/src/main.ts` module reference. Preview the CLI-owned metadata and manifest before any network write: ```bash napplet deploy --dry-run ``` Run `napplet deploy` only after the preview is correct and signing is configured. ## What stayed outside the napplet - Signing: `outbox.publish` asks the shell to sign the event. - Relay fanout: `toOutbox: true` lets the shell use the user's write relays. - Draft persistence: `storage.instance` is scoped by the shell to this napplet instance. - User identity: `identity.getPublicKey` returns the shell-user pubkey snapshot; `identity.onChanged` listens for shell-pushed signer changes. If a future feature needs follows, reactions, list mutations, direct messages, or counts, use the highest-level NAP that owns that user intent. Do not add a relay client, private-key handling, or browser storage layer to the napplet. --- # Core concepts These are the ideas that recur across every `@napplet/*` package. Understanding them once makes the rest of the docs straightforward. ## The JSON envelope Every message between a napplet and its shell is a JSON object with a `type` field in `domain.action` format: ```ts { type: "domain.action", ...payload } ``` Before any napplet script runs, the runtime opens a `MessageChannel`, keeps one port private, and hands the other to the shell once in a `napplet.hello` window message. The shim then sends and receives every envelope over that port. Request / response pairs are correlated by an `id` field. The base type is [`NappletMessage`](https://napplet.run/docs/packages/core.md) in `@napplet/core`; each NAP domain extends it with its own concrete message types. ```ts // outbound: napplet -> shell { type: 'outbox.publish', id: 'q1', event: { /* EventTemplate */ } } // inbound: shell -> napplet { type: 'outbox.publish.result', id: 'q1', ok: true, event: { /* NostrEvent */ } } ``` Unrecognized message types are silently ignored, which is what lets a napplet talk to an older or smaller shell and degrade gracefully. ## NAPs A **NAP** (*Nostr Applet Protocol*) is one capability contract between a napplet and its runtime: it defines a message domain, the valid `type` strings within it, the payload shapes, and the expected shell behavior. A NAP named `foo` owns all `foo.*` messages. NAP contracts are proposed and maintained in the [NAPs track](https://github.com/napplet/naps). In this SDK, each NAP is a **domain** — `relay`, `storage`, `inc`, `identity`, and so on — implemented as a subpath of [`@napplet/nap`](https://napplet.run/docs/packages/nap.md). The core dispatcher routes inbound messages to the right handler by domain prefix via `registerNap(domain, handler)` and `dispatch(message)`. See the [NAP domain reference](https://napplet.run/docs/naps/index.md) for the full list. ## Convention identities and URI bindings `napplet:/` is the stable convention identity. Manifest contracts, subscriptions, handler metadata, discovery, normalized wire messages, and routing use the complete queryless string with exact equality. INC `emit(topic, payload?)` is the developer-facing convention URI boundary. A queried URI such as `napplet:profile/open?pubkey=abc123` is normalized before `postMessage`: unique percent-decoded pairs become text payload fields, while literal `+` remains `+`. Fragments, malformed encoding, repeated decoded names, and a query combined with an explicit payload reject. Structured data uses an explicit payload with a queryless URI. Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). ```ts const result = await window.napplet.intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' } }); if (!result.ok) console.error(result.error); ``` Delivery does not depend on the source staying alive or on NAP-INC. Target startup/reuse, overlap, replacement, retry, and persistence remain runtime policy. This non-normative guide defers to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md), [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md), and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). ## The shell The **shell** is the trusted host application. It brokers signing — to a remote signer (NIP-46), a browser extension (NIP-07), or its own key management — plus relay access and persistent storage, and it hosts napplets in sandboxed iframes. Every sensitive operation a napplet wants — publish an event, read storage, fetch external bytes — is a request to the shell, which enforces policy, brokers it to wherever the capability is handled, and responds. Keys live wherever the shell delegates them; the napplet never sees them. The reference runtime is **Kehto** ([github.com/kehto/web](https://github.com/kehto/web)). Any compatible shell can host any napplet. ## The sandbox model Napplets run in an iframe sandboxed with **`allow-scripts` only** — crucially **no `allow-same-origin`**. (Shells MAY add tokens like `allow-forms` or `allow-popups` per their own policy.) The consequences: - No real origin, so no service worker registration, no same-origin storage. - No access to the shell's DOM, cookies, `localStorage`, `sessionStorage`, `IndexedDB`, or service workers. - No direct WebSocket / `fetch` to arbitrary origins (the shell's CSP enforces this). External bytes are fetched through the [resource NAP](https://napplet.run/docs/naps/resource.md) instead. All persistent and privileged state flows through the shell's proxies. The browser enforces the boundary — it is not a matter of the napplet behaving. ## Identity The shell computes each napplet's `(dTag, artifactHash)` from the verified NIP-5D manifest and artifact bytes before the iframe runs. It accepts one `napplet.hello` from the `contentWindow` of an iframe it created and binds that identity to the port the hello carries, so it knows which napplet a message came from by the port it arrived on. The binding lasts as long as that one document: a second hello, any other window-level message, a second `load` or the port closing makes the shell revoke the identity and its grants and remove the frame. See [NIP-5D §Identity](https://github.com/nostr-protocol/nips/pull/2303). ## ACL and storage Shells enforce capability, consent and storage policy using the verified napplet identity and each NAP's contract. NIP-5D's current identity is `(dTag, artifactHash)`, where `artifactHash` covers the artifact bytes alone. Any metadata embedded in the HTML participates in that digest; event-only metadata does not. Domain declarations do not grant authority. Moving from legacy aggregate hashes changes identity keys even when artifact bytes stay the same. Shell maintainers need an explicit migration policy for existing saved data and grants; the SDK does not silently copy them. See [event migration](https://napplet.run/docs/guide/event-migration.md) and the living [NAP contracts](https://github.com/napplet/naps) for domain-specific behavior. ## Domain Presence Because shells implement only the NAP domains they choose to, a napplet must **feature-gate** before using a domain. NIP-5D runtimes inject `window.napplet` before napplet code runs; each available NAP domain is present as a property, and unavailable domains are absent: ```ts if (window.napplet?.relay) { /* relay ops available */ } if (window.napplet?.identity) { /* identity queries available */ } if (window.napplet?.inc) { /* … */ } ``` This pairs with declarative negotiation: declare what you need in the manifest (`R` tags via [`@napplet/vite-plugin`](https://napplet.run/docs/packages/vite-plugin.md)), and gate at runtime with property presence. Always degrade gracefully when a capability is absent. --- # Migrating NIP-5D events This is non-normative migration guidance for the `@napplet` packages. The living [NIP-5D Manifest and Identity sections](https://github.com/dskvr/nips/blob/nip/5d/5D.md#manifest), through [PR #2303](https://github.com/nostr-protocol/nips/pull/2303), remain authoritative. Consult the [NAP track](https://github.com/napplet/naps) for domain operations. ## Release cutoff Packages have independent versions. The following releases switch their default writer or reader to the artifact-hash schema: | Package | Last legacy default series | First current-schema release | | --- | --- | --- | | `@napplet/cli` | `0.6.x` | `0.7.0` | | `@napplet/vite-plugin` | `0.14.x` | `0.15.0` | | `@napplet/conformance` | `0.17.x` | `0.18.0` | | `@napplet/conformance-cli` | `0.2.x` | `0.3.0` | These are minor releases because changing the event contract is breaking on 0.x. SDK, shim and NAP message APIs do not gain a legacy event mode. Conformance readers accept current events; older events need migration or older tooling. ## What changes The kinds remain `5129` (snapshot), `15129` (root), and `35129` (named). This is a schema change within those kinds. | Legacy event | Current event | | --- | --- | | `path /index.html ` and aggregate `x` | One `x ` | | `description` tag | Non-empty plain-text `content` | | `requires ` | Required `R ` or optional `O ` | | `archetype ` | Independent `z ` and `i [parameter names…]` | Never rename the aggregate hash to the artifact hash: they differ even for a single file. A new build hashes the final `index.html` bytes. Migration takes the artifact hash from the signed legacy `/index.html` path. Optional icon metadata is separate from artifact identity. Invalid or unavailable icons fall back to generic artwork rather than blocking the napplet. Descriptions are literal text. Display them with text APIs rather than HTML or Markdown rendering. Required and optional domains advertise integrations; neither grants access. A shell evaluates the complete `R` set locally and checks its own policy, rather than treating relay filter matches as proof of compatibility. ## Deploy an existing project Upgrade the Vite plugin and CLI. Supply a meaningful `description` option or HTML description meta for builds and `metadata.description` in `.napplet/config.yaml` for deployment overrides. The existing `requires` and `archetypes` config names remain valid authoring options; current output translates them to the new tags. `metadata.optional` and the plugin's `optional` option mark integrations that can be absent. ```sh napplet deploy --dry-run --format current napplet deploy --format current ``` An interactive deploy asks for `current` or `legacy`, defaulting to `current`. Unattended or JSON-output runs use `current` unless `--format legacy` is supplied. Review the dry-run event content, tags and artifact hash before publishing. For a shell still expecting the old schema: ```sh napplet deploy --dry-run --format legacy napplet deploy --format legacy ``` Legacy output is temporary and CLI-only. It emits path tags, the historical aggregate, description and requires/archetype tags. Optional domains become legacy requirements because that schema cannot express optionality; intent parameter advertisements are not representable. Do not choose legacy when those distinctions are needed. Independent intent advertisements with multiple or no roles require explicit `metadata.archetypes` pairs for legacy deployment; the CLI will not invent pairings. No removal release is scheduled here; a future breaking CLI release can remove the isolated serializer and format choice. ## Prepare an event migration Export the complete signed event JSON. The CLI verifies its signature and prepares an unsigned review file without fetching, executing, rewriting or uploading the artifact: ```sh napplet migrate old-event.json --optional theme --output migration-preview.json # Add --description "A meaningful description" when the old event lacks one. ``` The preview contains `sourceId`, `sourcePubkey`, `template` and review notes. Existing output files are never overwritten. `--optional` can only reclassify a domain already declared by the source; all other legacy requirements remain required. Unknown extension tags are retained. Invalid optional icons are removed with a note. Ambiguous identifiers, multiple artifact paths, invalid signatures and missing descriptions fail with an error. The command does not sign or publish. Review the template, then use your event signing/publication tooling or the [migration web app](https://github.com/napplet/migrate). That app has its own documented spec revision; check its output against the live NIP before publishing. Signing with the original author preserves a replaceable event's address. A different signer creates a different address; snapshots always produce a new event ID. Keep the original signed event and deployment files for rollback. ## Compatibility beyond the CLI - Shell ACL, consent and storage/config keys may incorporate identity hashes. The new artifact hash differs from the legacy aggregate even when artifact bytes are unchanged. Shell maintainers need an explicit verified data/grant migration policy; these packages do not silently transfer grants or saved data. - Immutable legacy `nevent` pointers continue to reference legacy snapshots. Publish and share a new pointer. Named addresses can stay the same when the author and `d` identifier stay the same, but legacy readers may stop loading their newest event. - Discovery code that filters `requires` or `archetype` needs `R`/`O` and `z`/`i`. During rollout, legacy catalog ingestion belongs in an explicit boundary; it must not silently treat the old aggregate as `x` artifact identity. - The `.nip5a-manifest.json` filename and `nip5aManifest` helper remain compatibility names for local tooling. Current conformance checks derive identity from the event and downloaded artifact, not sidecar convenience fields. NAP-INTENT now derives role and action from convention URIs, catalogs NIP-5D `z`/`i` advertisements, and exposes retained target delivery through `onDelivery`. NAP-SHELL readiness is optional and does not gate other domains. Follow the living NAP documents for those API contracts. The CLI still applies its existing lowercase/hyphen naming policy when selecting deployment names. That is a tool limitation, not a NIP-5D restriction on `d` values; migration preserves identifiers exactly. --- # Getting started This page walks you from zero to a deployable napplet. The standalone `napplet` CLI provisions its own cached tooling and does not require host Node.js, npm, npx, or Deno, including for screenshots and conformance. You will need Node.js 20+ and a package manager to develop the generated Vite project or use the optional skills.sh CLI. To actually *run* a napplet you also need a host shell — see [running in a shell](#running-in-a-shell) at the end. ## 1. Install the CLI ```bash # macOS or Linux curl -fsSL https://napplet.run/install.sh | sh ``` ```powershell # Windows PowerShell irm https://napplet.run/install.ps1 | iex ``` Each installer downloads the platform binary and verifies it against the release's `SHA256SUMS` before replacing `napplet`. The [`@napplet/cli`](https://napplet.run/docs/packages/cli.md) page documents the JSR/Deno alternative. Run `napplet guide` at any point for the complete command sequence and links to the documentation for each stage. ## 2. Create the project ```bash napplet create my-napplet cd my-napplet ``` `napplet create` delegates to the maintained [`github.com/napplet/boilerplate`](https://github.com/napplet/boilerplate) template. The generator creates the project and derives only its package name; it does not own deployment name, title, or archetype metadata. ## 3. Initialize deployment metadata ```bash napplet init ``` The wizard writes `.napplet/config.yaml` with the named-manifest d-tag, title, optional description, queryless `napplet:/` convention metadata, relays, and Blossom servers. Automation can provide the same values explicitly: ```bash napplet init --name my-napplet --title "My Napplet" \ --archetype napplet:note/open \ --relay wss://relay.example --server https://blossom.example ``` The CLI validates the documented `napplet:/` shape and deploys the complete queryless convention identity. At runtime, INC `emit` and intent `invoke/open` may accept `napplet:/?name=value` developer input. Their bindings transpose query pairs to text payload fields before sending a queryless normalized identity. Subscriptions, manifest discovery, and handler routing do not parse queries and use exact equality. ## 4. Install agent skills ```bash npx skills add napplet/web ``` The open [skills CLI](https://skills.sh) detects the coding agents on your machine (Claude Code, Codex, Cursor, Gemini CLI, Copilot, and many more) and installs the `napplet-*` skills where each agent reads them. Add `-g` for a global install, `--skill napplet-make` for a single skill, or `-a claude-code` to target one agent. See [Agent skills](https://napplet.run/docs/guide/agent-skills.md) for what each skill does. ## 5. Build and verify ```bash pnpm install # Ask your agent to build the napplet. pnpm verify pnpm test:conformance ``` ## 6. Preview, then deploy ```bash napplet deploy --dry-run napplet deploy ``` The dry run resolves the same config, files, metadata, and manifest events without uploading or publishing. Configure a local or remote signer before the live deploy; see [`@napplet/cli`](https://napplet.run/docs/packages/cli.md#deploy). Choose the Note Drafts path that matches how you want to learn: - [Build a Note Drafts napplet from scratch](https://napplet.run/docs/guide/build-note-drafts-napplet.md) hand-wires every file so you can see the runtime boundary one piece at a time. - [Build a Note Drafts napplet from boilerplate](https://napplet.run/docs/guide/build-note-drafts-napplet-from-boilerplate.md) starts from the generator, preserves its project substrate, and replaces the starter demo with the same note composer. - [Build a Note Drafts napplet with an AI agent and the napplet skills](https://napplet.run/docs/guide/build-note-drafts-napplet-with-ai-agent-and-skills.md) shows the prompt, review loop, and verification commands for agent-assisted authoring. All three create a small Nostr note composer that autosaves through `storage`, reads the shell-user pubkey through `identity`, publishes through `outbox`, and runs in Kehto/Paja. ## Retrofit an existing app manually For a new napplet, prefer the generator above. It owns the package manager pin, Vite config, single-file build plumbing, scripts, conformance wiring, and starter layout. Use manual wiring only when you are adding napplet support to an existing app. Install the napplet-side SDK: ```bash npm install @napplet/sdk ``` And the build-time Vite plugin as a dev dependency, which generates the signed NIP-5A manifest event: ```bash npm install -D @napplet/vite-plugin ``` ```ts // vite.config.ts import { defineConfig } from 'vite'; import { nip5aManifest } from '@napplet/vite-plugin'; export default defineConfig({ plugins: [nip5aManifest({ nappletType: 'my-napplet', artifactMode: 'single-file' })], }); ``` Mirror the boilerplate scripts so verification stays consistent: ```jsonc { "scripts": { "build": "vite build", "type-check": "tsc --noEmit", "verify": "npm run type-check && npm run build", "test:conformance": "npm run build && napplet-conformance ./dist" } } ``` ## Build your first napplet NIP-5D runtimes inject `window.napplet` before your code runs. Napplet code can use the injected namespace directly: ```ts // main.ts // Read kind 1 notes through the shell's outbox-aware routing const { events } = await window.napplet.outbox.query( [{ kinds: [1], limit: 20 }], { timeoutMs: 3000 }, ); for (const result of events) console.log('Note:', result.event.content); // Subscribe to live updates through the same outbox boundary const sub = window.napplet.outbox.subscribe([{ kinds: [1], limit: 20 }], { timeoutMs: 3000, }); sub.on('event', (result) => console.log('New note:', result.event.content)); // Publish a note — the shell signs and fans it out; your napplet never sees the key const published = await window.napplet.outbox.publish({ kind: 1, content: 'Hello from my napplet!', tags: [], created_at: Math.floor(Date.now() / 1000), }); if (!published.ok) throw new Error(published.error ?? 'publish failed'); ``` Notice you never imported a signing library and never touched a relay socket. The runtime sends `outbox.query`, `outbox.subscribe`, and `outbox.publish` JSON envelopes; the shell does relay discovery, signing, fanout, and policy. ## Runtime injection vs. SDK Runtimes and napplets use different packages. | | [`@napplet/shim`](https://napplet.run/docs/packages/shim.md) | [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) | | --- | --- | --- | | **Import style** | `import { installNappletGlobal } from '@napplet/shim'` | `import { outbox, inc, intent } from '@napplet/sdk'` | | **Who uses it** | Host runtime before napplet scripts execute | Napplet application code | | **What it does** | Injects selected `window.napplet.` objects | Named, typed exports that wrap injected domains | | **Required?** | Required for runtimes that use these packages | Optional convenience for bundler users | The **sdk** is an ergonomic, typed wrapper. Its methods delegate to `window.napplet.*` at call time and throw a clear error if the runtime did not inject the namespace or requested domain. Typical napplet code imports only the SDK: ```ts import { outbox, inc, intent, storage } from '@napplet/sdk'; ``` If you're writing a vanilla napplet with no build step, you can skip the sdk and use the injected `window.napplet.*` namespace directly. For cross-napplet intent, call `intent.invoke(uri, options?)` or `intent.open(uri, options?)` with a convention URI such as `napplet:profile/open?pubkey=abc123`. An `ok: true` result means the runtime accepted delivery responsibility; it does not mean the target has already handled anything. An `ok: false` result carries `error`. The target receives the delivery through `intent.onDelivery(handler)` as a separate push with a runtime-attested `sender`, independent of the source lifetime and without any NAP-INC dependency. This is non-normative orientation; the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md), [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md), and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md) documents are authoritative. ## Running in a shell A napplet only does something when hosted by a compatible **shell** — the trusted runtime that owns keys, relays, and storage and proxies your requests. The reference runtime is **Kehto**. > **TIP** > > The reference shell/runtime is **Kehto**: > [github.com/kehto/web](https://github.com/kehto/web). > You can also find napplet projects under the > [github.com/napplet](https://github.com/napplet) organization. > For questions and coordination, join the > [community group chat](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt). Next, read [Core concepts](https://napplet.run/docs/guide/concepts.md) to understand the envelope, NAPs, the sandbox model, identity, and storage scoping. ### Terminal and automation options `napplet init` and `napplet deploy` guide you through missing settings in a terminal. Deployment previews and results use readable sections, and network deployments show upload and relay progress. For automation, add `-i` / `--non-interactive` and supply values through flags or configuration. CI and redirected streams never trigger prompts. Use `napplet deploy -i --json` for machine-readable results and `--no-color` for plain display. See the [CLI reference](https://napplet.run/docs/packages/cli.md) for all options. --- # What are napplets? A **napplet** is a small, single-purpose Nostr web application that runs inside a sandboxed iframe and delegates everything sensitive — signing, storage, relay access — to a host **shell**. The shell and the napplet communicate over `postMessage` using a JSON envelope wire format defined by [NIP-5D](https://napplet.run/docs/guide/nip-5d.md). > **ALPHA** > > napplet is **alpha**. The specification is experimental and a moving target. > There **will most certainly be drift** between the packages and the > specification. Things **will most certainly change**. For adventurers only. ## The monolith problem Today, every Nostr client re-implements the same machinery from scratch: a feed viewer, a DM inbox, a profile editor, relay management, key handling, signing. The result is a handful of large monolithic clients that each duplicate the same hard, security-sensitive plumbing — and each one is a fresh place for that plumbing to go wrong. NIP-5D proposes a different shape: > Napplets SHOULD be single-purpose rather than monolithic. The shell composes > multiple napplets; napplets don't compose themselves. ## Shell + napplets Instead of one client that does everything, you get: - A **shell** — the trusted host application. It brokers signing — to a remote signer (NIP-46), a browser extension (NIP-07), or its own key management — plus relay access and persistent storage. It hosts napplets in sandboxed iframes and brokers their requests. - Many **napplets** — untrusted, single-purpose mini-apps. A chat widget, a feed viewer, a profile editor, a relay manager — each one its own napplet. The napplet never calls a signer, relay connection, or `localStorage` directly. It asks the shell, which brokers each request through **NAP** interfaces — keys live wherever the shell delegates them, not inside the napplet. The browser's iframe sandbox (`allow-scripts` only, **no** `allow-same-origin`) enforces the boundary, so a compromised napplet cannot reach the shell's DOM, cookies, or key material. ## NIP-5D in one paragraph NIP-5D defines the napplet-shell protocol. All messages are JSON objects with a `type` field in `domain.action` format - `{ type: "outbox.query", ...payload }` - sent over a `MessagePort` that the runtime hands to the shell in a single `napplet.hello` before any napplet script runs. The shell binds that port to the `(dTag, artifactHash)` it verified from the napplet's NIP-5D manifest and artifact bytes, and revokes the binding if the frame's document is replaced. Capability domains (**NAPs** — modular *Nostr Applet Protocol* contracts) are negotiated declaratively via manifest `R` tags and queried at runtime with `window.napplet?.domain`. Read the full [NIP-5D explanation](https://napplet.run/docs/guide/nip-5d.md). Archetype roles use stable identities such as `napplet:note/open`. Manifest discovery, subscriptions, normalized wire identities, and handler routing keep that complete queryless string and match it exactly. INC `emit` may transpose URI query pairs into a text payload before sending the queryless identity. Intent invocation uses `invoke(uri, options?)` or `open(uri, options?)`; its structured result reports both dispatch success and whether a handler accepted it. Payload remains untrusted at the receiving boundary. This project follows [NAP-INC PR #89 (`4593ce9`)](https://github.com/napplet/naps/pull/89/commits/4593ce9e301ce098fd3dad64206fcd6f144fa7af), [the governance/web projection PR #90 (`896c32c`)](https://github.com/napplet/naps/pull/90/commits/896c32c92deee68dc4d10fc1132b62df20cccb6f), and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). ## A bigger pool of builders Because the hard, security-sensitive Nostr machinery — relay pools, NIP-46 signers, event signing and encryption — lives behind the shell, a napplet author doesn't need deep protocol expertise to ship something useful. They render UI and send envelopes; the shell handles the dangerous parts. That lowers the barrier to entry and widens the ecosystem **without** putting users at risk. ## Web first, not web only The napplet model is being proven on the web first, and for good reasons: the iframe is a real, browser-enforced sandbox you can demonstrate today; the web has the largest developer ecosystem to draw from; and a napplet is just static files, so it distributes as a [NIP-5A](https://github.com/nostr-protocol/nips) static site (an **nsite**) over Nostr + Blossom with no servers. **NIP-5D is specifically that web realization** — it defines the security posture and transport (`postMessage` across an `iframe` boundary) for napplets in the NIP-5A context. It is a web-context spec, not a cross-platform one. The broader **napplet** and **NAP** capability model, however, is deliberately transport-agnostic — capability domains and intent, not browser plumbing. The web is the starting point, not the ceiling: the same model leaves room to be carried on other platforms later, each of which would define its own transport and security posture (its own analogue to NIP-5D) rather than reusing the web one. ## Who it's for - **Nostr developers** who want to ship a focused app without re-building relay management, signing, and storage — and without ever holding the user's keys. - **Developers new to Nostr** who want to build for the ecosystem without mastering every protocol detail first — the shell carries that weight. - **Shell / client authors** who want to host third-party apps safely, competing on trust and UX rather than feature count. - **Protocol tinkerers** exploring what a composable, capability-based Nostr client ecosystem looks like. Ready to build? Head to [Getting Started](https://napplet.run/docs/guide/getting-started.md), or join the [community group chat](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt). --- # NIP-5D explained [NIP-5D — *Nostr Web Applets*](#source-of-truth) is the specification that defines how a napplet and its host shell talk to each other. This page summarizes the model the `@napplet/*` packages implement. > **NON-NORMATIVE — read the spec** > > This page is an orientation, not the specification. The living, authoritative > documents are **[NIP-5D (PR #2303)](https://github.com/nostr-protocol/nips/pull/2303)** > and the **[NAPs track](https://github.com/napplet/naps)**. For every normative > requirement ("MUST", message shapes, manifest fields), defer to those — not to > this page, the packages, or any test. ## Philosophy - Napplets are **small and single-purpose** rather than monolithic — a chat widget, a feed viewer, a profile editor, a relay manager are each separate applications. - The **shell composes** multiple napplets; napplets do not compose themselves. - The protocol is intentionally minimal: a JSON envelope over `postMessage`, with capabilities layered on top through an extension framework. ## Terminology | Term | Meaning | | --- | --- | | **Shell** | The trusted hosting web application that contains napplet iframes. | | **Napplet** | A sandboxed iframe application that communicates via `postMessage`. | | **dTag** | The napplet type identifier from its NIP-5D manifest. | | **Artifact hash** | SHA-256 of the final `/index.html` bytes. | | **NAP** | *Nostr Applet Protocol* — one capability contract between a napplet and its runtime, defining the protocol messages for a capability domain. | > In this SDK, each NAP is a **domain** that owns one message domain (`relay`, > `storage`, `identity`, …). NAP contracts live in the > [NAPs track](https://github.com/napplet/naps). ## Transport - Before any napplet script runs, the shell-injected bootstrap creates a `MessageChannel`, keeps one port private, and hands the other to the shell exactly once: `window.parent.postMessage({ type: "napplet.hello" }, '*', [port])`. - That handoff is the only window-level message a napplet sends. Every NAP message in both directions travels over the port. Shells do not send NAP messages with `iframeWindow.postMessage` and do not transfer a port into the iframe. - Napplets MUST be embedded with `sandbox="allow-scripts"` and **without** `allow-same-origin`. - Napplets have **no access** to `localStorage`, `sessionStorage`, `IndexedDB`, direct WebSocket connections, or signing keys. - Shells MUST **not** provide `window.nostr` (NIP-07). Signing and encryption are brokered by the shell instead — to a remote signer, an extension, or the shell's own key management, depending on the shell. ## Wire format Every message is a JSON object with a `type` field in `domain.action` format: ```ts { type: ".", ...payload } ``` Request/response pairs are correlated by an `id` field: ```ts // request { type: "outbox.query", id: "abc", filters: [{ kinds: [1] }] } // a result arrives back with a matching id { type: "outbox.query.result", id: "abc", events: [{ event: { /* NostrEvent */ } }] } ``` The `type` prefix before the first `.` is the **domain**, and routes the message to the correct NAP handler. **Unrecognized message types are silently ignored** for forward compatibility — a napplet can speak to an older shell and degrade gracefully. ## Identity - The shell computes `(dTag, artifactHash)` from the signed NIP-5D manifest and the verified artifact bytes **before** the iframe runs, and injects those bytes with the bootstrap via `srcdoc`. - It accepts a `napplet.hello` only when `MessageEvent.source` is the `contentWindow` of an iframe it created and exactly one port is attached, and binds the identity to **that port**. Origin checks use the port binding, not `event.origin`. - Identity belongs to the verified **document**, not the iframe or its `Window`: a `Window` survives same-frame navigation and an injected `meta` CSP does not carry over, so a napplet could otherwise replace itself with unverified content and keep its grants. - A second hello, any other window-level message, a second `load` event or the port closing tells the shell the document was replaced. The shell closes the port, revokes the identity and every grant attached to it, and removes the iframe. It may then create a fresh iframe from the verified bytes. ## Manifest and NAP negotiation The current manifest kinds are 5129, 15129, and 35129. The manifest carries one direct artifact hash in `x`, description text in `content`, required domains in `R`, and optional integrations in `O`. Archetype advertisements use `z`; accepted intents and parameter names use `i`. Optional presentation metadata rides on the same event: one content-addressed `icon`, any number of `screenshot` tags and at most one `preview` video (NIP-92 `key value` fields keyed by the blob's `x` hash), plus `source`, `source-commit` and `source-archive` pins; `server` tags hint where those blobs live. See [NIP-5D §Manifest](https://github.com/dskvr/nips/blob/nip/5d/5D.md#manifest) for the complete living contract. The plugin generates current events and the CLI offers temporary legacy output. See [event migration](https://napplet.run/docs/guide/event-migration.md) for release cutoffs and rollout implications. Runtime availability still comes from injected domain presence, such as `window.napplet?.relay`. ## NAP extension framework - A NAP spec defines a message **domain**, the valid `type` strings within it, the payload shapes, and the expected shell behavior. - A NAP named `foo` owns **all** `foo.*` message types. - Each NAP must be **independently implementable**, and shells may support any subset of NAPs. This is what makes the protocol modular: NAP contracts live in the [NAPs track](https://github.com/napplet/naps); see the [NAP domain reference](https://napplet.run/docs/naps/index.md) for the domains this SDK ships. ## Convention URI projection The stable convention identity is the complete queryless `napplet:/` string. Archetype manifest tags, subscriptions, handler discovery, normalized messages, and routing contain that identity and use exact equality. The web binding normalizes developer-facing URI input before `postMessage` for INC `emit(topic, payload?)`. Unique percent-decoded query pairs become text payload fields, literal `+` remains `+`, and the outgoing topic is queryless. Fragments, malformed percent encoding, repeated decoded names, and a query combined with explicit payload reject. Structured/non-text data uses a queryless URI with an explicit payload. Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). At this web projection's trust boundary, the host authenticates the napplet by the port bound in `napplet.hello` and derives the NAP-level `sender` from that endpoint. A napplet does not provide sender. On other carriers the endpoint mechanism differs, while the runtime-attested sender contract stays the same. Receivers treat sender as provenance and payload as untrusted. This non-normative guide defers to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md), [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md), and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). ## Security model - Napplets are **untrusted**; the shell is **trusted**; the **browser** enforces the iframe sandbox boundary. - Adding `allow-same-origin` would grant the napplet a real origin (letting it register a service worker, reach same-origin storage, etc.) — so it is **prohibited**. - The port handed over in `napplet.hello` is the sender identity. Only the verified document's bootstrap holds the other end, so a navigated or reloaded document has no NAP channel, identity or grants. - An injected `meta` CSP does not restrict a document navigating its own frame, so a napplet can disclose data it was given by encoding it in a navigation URL. Revocation stops further access but cannot recall what was sent; shells should treat data released to a napplet as disclosable by it. - Shells MUST **not** sign or broadcast events containing ciphertext received from a napplet. - The protocol does **not** protect against compromised browsers, malicious shells, or social engineering — it secures the napplet-shell boundary, not the shell itself. ## Source of truth This page is a **non-normative** summary. The living, authoritative documents are: - **NIP-5D** (the protocol): [github.com/nostr-protocol/nips/pull/2303](https://github.com/nostr-protocol/nips/pull/2303) - **NAPs track** (the capability domains): [github.com/napplet/naps](https://github.com/napplet/naps) For every normative requirement, read those — not this page. See [NIP-5D spec status](https://napplet.run/docs/spec.md) for how drift is tracked. --- # napplet: Composable Nostr apps Sandboxed Nostr web applets that delegate signing, storage, and relay access to a host shell over the NIP-5D JSON envelope wire format. - [Get Started](https://napplet.run/docs/guide/getting-started.md) - [What are napplets?](https://napplet.run/docs/guide/index.md) - [Community](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt) - [napplet.run](https://napplet.run/) ## Single-purpose by design A napplet does one thing well. The shell composes many sandboxed napplets instead of one monolithic client re-implementing feeds, DMs, profiles, and relay management. ## Every request brokered Napplets never call a signer, relay, or store directly. Every request is brokered through the shell over postMessage — to a remote signer, an extension, or the shell's own services. No window.nostr is installed. ## Browser-enforced sandbox The iframe uses allow-scripts only — no allow-same-origin. Identity is bound to the MessagePort the verified document hands over in napplet.hello, and revoked if the frame navigates. ## Modular NAP domains Capabilities are split into NAP domains (relay, storage, inc, identity, and more). Shells implement the domains they support; napplets gate on domain presence. ## Just a JSON envelope Every message is { type: "domain.action", ...payload }. A simple, standardized wire format that any shell can host and any web app can speak. ## Portable across shells Build once against the protocol. Any compatible shell — like the Kehto reference runtime — can host your napplet without changes. --- # BLE > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/62)** (status checked 2026-10-31). GATT binding has unit coverage; real-device behavior across host backends is not established by these tests. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Runtime-mediated Bluetooth LE/GATT sessions. Napplets receive opaque session ids and byte arrays while the shell owns chooser UI, permissions, device handles, GATT lifecycle, notifications, disconnects, and policy. ```ts if (window.napplet?.ble) { const { session } = await window.napplet.ble.open({ acceptAllDevices: true }); const services = await window.napplet.ble.services(session.id); } ``` This non-normative guide defers to the living [NAP-BLE proposal](https://github.com/napplet/naps/pull/62). Feature-gate with `if (window.napplet?.ble)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/ble`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # COMMON > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/67)** (status checked 2026-10-31). Social action binding is tested; broader host integration and evolving action semantics warrant explicit opt-in. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Common social actions — public NIP-19 helpers, profile lookup, follows, follow/unfollow, reactions, and reports. The shell owns identity, consent, event construction, signing, publishing, relay access, and NIP-19 handling. ```ts if (window.napplet?.common) { const { pubkeys } = await window.napplet.common.follows(); await window.napplet.common.react(eventId, '+'); } ``` This non-normative guide defers to the living [NAP-COMMON proposal](https://github.com/napplet/naps/pull/67). Feature-gate with `if (window.napplet?.common)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/common`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # CONFIG > **Implementation: exploratory · Proposal: [draft](https://github.com/napplet/naps/pull/14)** (status checked 2026-10-31). Configuration binding is tested, but the upstream draft is marked MAY DEFER; use for design exploration. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Declarative per-napplet configuration (JSON Schema-driven). The shell renders the settings UI, validates, persists, and pushes live values; the shell is the sole writer. ```ts const sub = window.napplet.config.subscribe((values) => applyTheme(values.theme as string)); window.napplet.config.openSettings({ section: 'appearance' }); ``` This non-normative guide defers to the living [NAP-CONFIG proposal](https://github.com/napplet/naps/pull/14). Feature-gate with `if (window.napplet?.config)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/config`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # COUNT > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/69)** (status checked 2026-10-31). Count binding is tested, but accuracy and aggregation across real relay backends remain outside that evidence. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Runtime-mediated aggregate counts for NIP-01 filters. The runtime owns relay COUNT support, indexes, caches, approximation, relay disclosure, and refusal policy. A result carries `ok`, and when available `count`, an `approximate` flag, an `hll` value, and the `relays` consulted. ```ts if (window.napplet?.count) { const result = await window.napplet.count.query({ kinds: [7], '#e': [eventId] }); if (result.ok) showReactions(result.count, result.approximate); } ``` This non-normative guide defers to the living [NAP-COUNT proposal](https://github.com/napplet/naps/pull/69). Feature-gate with `if (window.napplet?.count)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/count`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # CVM > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/31)** (status checked 2026-10-31). Session and registry bindings are tested; upstream explicitly marks the contract NEEDS WORK. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Native ContextVM bridge — MCP-over-Nostr (`discover` / `listTools` / `callTool` / `listResources` / `readResource` / `registry.*`); the shell owns transport, registry selection, and tool policy. ```ts if (window.napplet?.cvm) { const servers = await window.napplet.cvm.discover({ search: 'relay' }); const tools = await window.napplet.cvm.listTools(servers[0]); const result = await window.napplet.cvm.callTool(servers[0], tools[0].name, {}); } ``` This non-normative guide defers to the living [NAP-CVM proposal](https://github.com/napplet/naps/pull/31). Feature-gate with `if (window.napplet?.cvm)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/cvm`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # DM > **Implementation: experimental · Proposal: [draft](https://github.com/napplet/naps/pull/74)** (status checked 2026-10-31). Direct-message binding has shim and type coverage; transport interoperability is not established by those tests. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Runtime-mediated direct messages. The napplet presents direct-message UI while the runtime owns signing, encryption, relay routing, storage, key and session state, and policy. The napplet sees normalized conversations, history pages, send results, and live message delivery through `status`, `conversations`, `messages`, `send`, `subscribe` / `unsubscribe`, and `onMessage`. ```ts if (window.napplet?.dm) { const { conversations } = await window.napplet.dm.conversations({ limit: 20 }); window.napplet.dm.onMessage((message) => render(message)); await window.napplet.dm.subscribe(); await window.napplet.dm.send({ recipients: [''], content: 'hello' }); } ``` This non-normative guide defers to the living [NAP-DM proposal](https://github.com/napplet/naps/pull/74). Feature-gate with `if (window.napplet?.dm)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/dm`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # FS > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/88)** (status checked 2026-10-31). Filesystem binding has MVP coverage; portability and authorization across storage backends remain under evaluation. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Shell-mediated virtual filesystem access. Napplets discover visible roots, ask the runtime to mediate file and directory selection, inspect and list entries, read and write base64-encoded file bytes, create, remove and move them, and subscribe to advisory change events. The runtime owns host paths, mounts, backing store, normalization, policy, and authorization — the napplet sees only virtual paths. `info()` is advisory discovery, not an authorization token: permissions can change mid-session and any operation can still fail. ```ts if (window.napplet?.fs) { const picked = await window.napplet.fs.pickFile({ accept: [{ extension: '.md' }] }); const bytes = await window.napplet.fs.read(picked.entries[0].path); await window.napplet.fs.write('/shared/copy.md', bytes.data, { mode: 'replace' }); const entries = await window.napplet.fs.list('/shared'); const watchId = await window.napplet.fs.watch('/shared', { recursive: true }); window.napplet.fs.onChanged((change) => refresh(change.path)); } ``` `fs.write.data` and `FsReadResult.data` carry bytes as RFC 4648 standard padded base64 text on the JSON wire. Byte limits and result counts are decoded-byte counts. This non-normative guide defers to the living [NAP-FS proposal](https://github.com/napplet/naps/pull/88). Feature-gate with `if (window.napplet?.fs)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/fs`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # GAMEPAD > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/130)** (status checked 2026-10-31). Cross-repository delivery includes binding, security, runtime, and conformance verification; the reopened proposal is still subject to change. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). ### Controller snapshots This non-normative guidance defers to [NAP-GAMEPAD](https://github.com/napplet/naps/pull/130). When the runtime exposes `window.napplet.gamepad`, the SDK provides cached `getGamepads()`, read-only `available` and `focused`, and `onChange(handler)` with an independently closable listener. Keep keyboard or pointer controls available when the domain or native controllers are unavailable. Ignore gameplay edges while unfocused, and reset the button baseline on regained focus. Preserve null slots and route controllers by their `index`. ```ts import { gamepad } from '@napplet/sdk/gamepad'; if (window.napplet?.gamepad) { const subscription = gamepad.onChange(state => { if (!state.available || !state.focused) return; for (const pad of state.pads) { if (pad) console.log(pad.index, pad.axes); } }); // Call subscription.close() when the owning view is disposed, not right away. } ``` Declare `gamepad` as optional (`O`) when other controls fulfill the core task. Use a hard requirement (`R`) only when the core task needs controller input. The runtime owns controller access and focus isolation; app code does not call native `navigator.getGamepads()` or open raw HID devices. Haptics and remapping are outside this proposal. --- # IDENTITY > **Implementation: supported · Proposal: [merged](https://github.com/napplet/naps/pull/102)** (status checked 2026-10-31). Read-only identity queries and updates have focused binding and reference-runtime coverage. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Read-only user queries — pubkey, profile metadata, follows, … Never signs, encrypts, or decrypts. ```ts const pubkey = await window.napplet.identity.getPublicKey(); const profile = await window.napplet.identity.getProfile(); const sub = window.napplet.identity.onChanged((pk) => reload(pk)); ``` This non-normative guide defers to the living [NAP-IDENTITY proposal](https://github.com/napplet/naps/pull/102). Feature-gate with `if (window.napplet?.identity)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/identity`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). ## Changes and profile artwork User identity changes report the connected signer, including an empty string at sign-out; they do not alter napplet identity. Profile picture and banner bytes use the optional [`resource`](https://napplet.run/docs/naps/resource.md) domain when exposed; otherwise show local placeholder artwork. See the living [NAP-IDENTITY](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md). --- # INC > **Implementation: supported · Proposal: [merged](https://github.com/napplet/naps/pull/104)** (status checked 2026-10-31). Topic and channel bindings have compatibility tests and authenticated-endpoint reference-runtime coverage. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Inter-napplet communication — topic-based publish/subscribe between napplets. ```ts // The INC binding accepts a developer-facing convention URI when emitting: window.napplet.inc.emit('napplet:profile/open?pubkey=abc123'); // The runtime sends topic `napplet:profile/open` with { pubkey: 'abc123' }. // Another napplet subscribes to that exact stable topic: const sub = window.napplet.inc.on('napplet:profile/open', (event) => { // `event.sender` is supplied by the runtime from the authenticated source endpoint. const target = (event.payload as { pubkey?: string }).pubkey; }); ``` `emit(topic, payload?)` transposes unique percent-decoded query pairs into a shallow text payload before posting the normalized message. Literal `+` stays `+`. Consumers subscribe to the stable queryless topic, and routing then uses exact equality with no query-aware, wildcard, or prefix matching. Fragments, malformed percent encoding, duplicate decoded names, and query plus explicit payload reject. Use a queryless topic with an explicit payload for structured or non-text data. This non-normative guide defers to the living [NAP-INC proposal](https://github.com/napplet/naps/pull/104). Feature-gate with `if (window.napplet?.inc)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/inc`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). INC is defined by the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) and its [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). For URI-addressed handler routing, see [`intent`](https://napplet.run/docs/naps/intent.md). --- # NAP domain reference A **NAP** (*Nostr Applet Protocol*) is one capability contract between a napplet and its runtime — what the runtime provides (relay access, storage, intents, …) and exactly how a napplet asks for it. On the web, [NIP-5D](https://napplet.run/docs/guide/nip-5d.md) binds each NAP to a **message domain**: a NAP named `foo` owns all `foo.*` JSON envelope messages, their payload shapes, and the expected shell behavior. NAP contracts are proposed and maintained in the [NAPs track](https://github.com/napplet/naps). This reference is non-normative. Every domain page defers to its living proposal in the NAPs track, which governs protocol behavior. The protocol is modular by design. A NAP must be **independently implementable**, and shells may support **any subset** of NAPs. That's why napplets feature-gate with injected domain property presence before using a domain and degrade gracefully when it's absent — see [Core concepts](https://napplet.run/docs/guide/concepts.md#domain-presence). ## Domains by purpose Each domain has its own page with its purpose, a minimal example, and its implementation maturity. Examples assume the runtime injected `window.napplet` before napplet code ran; the same calls are also importable as named helpers from [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md), and each domain ships as a subpath of [`@napplet/nap`](https://napplet.run/docs/packages/nap.md) (barrel / `types` / `shim` / `sdk`). ### Runtime and environment | Domain | Purpose | Maturity | | --- | --- | --- | | [`shell`](https://napplet.run/docs/naps/shell.md) | Optional environment snapshot, readiness, and domain presence checks. | supported | | [`theme`](https://napplet.run/docs/naps/theme.md) | Read-only shell theme — colors, fonts, background, and live updates. | supported | | [`config`](https://napplet.run/docs/naps/config.md) | Schema-driven per-napplet settings rendered and persisted by the shell. | exploratory | | [`keys`](https://napplet.run/docs/naps/keys.md) | Named actions and keyboard bindings owned by the shell. | experimental | | [`notify`](https://napplet.run/docs/naps/notify.md) | Shell-rendered notifications, badges, and action callbacks. | experimental | | [`media`](https://napplet.run/docs/naps/media.md) | Media sessions with shell-side playback controls. | supported | | [`link`](https://napplet.run/docs/naps/link.md) | User-visible external link opening under shell policy. | supported | ### Identity and social | Domain | Purpose | Maturity | | --- | --- | --- | | [`identity`](https://napplet.run/docs/naps/identity.md) | Read-only user queries — pubkey, profile, follows. Never signs. | supported | | [`common`](https://napplet.run/docs/naps/common.md) | Common social actions — follows, reactions, reports, NIP-19 helpers. | experimental | | [`lists`](https://napplet.run/docs/naps/lists.md) | NIP-51 list add/remove with runtime-owned signing and encryption. | experimental | | [`dm`](https://napplet.run/docs/naps/dm.md) | Direct-message conversations, history, and sending via the runtime. | experimental | ### Nostr events and relays | Domain | Purpose | Maturity | | --- | --- | --- | | [`outbox`](https://napplet.run/docs/naps/outbox.md) | Outbox-aware reads and publishes — the default event boundary. | supported | | [`relay`](https://napplet.run/docs/naps/relay.md) | Low-level relay proxy for explicit relay-local behavior. | supported | | [`count`](https://napplet.run/docs/naps/count.md) | Aggregate counts for NIP-01 filters. | experimental | ### Napplet to napplet | Domain | Purpose | Maturity | | --- | --- | --- | | [`inc`](https://napplet.run/docs/naps/inc.md) | Topic-based publish/subscribe between running napplets. | supported | | [`intent`](https://napplet.run/docs/naps/intent.md) | URI-addressed intents routed to a handler napplet. | supported | ### Data, files, and network | Domain | Purpose | Maturity | | --- | --- | --- | | [`storage`](https://napplet.run/docs/naps/storage.md) | Scoped key-value storage isolated per napplet. | supported | | [`fs`](https://napplet.run/docs/naps/fs.md) | Virtual filesystem — pickers, reads, writes, and change events. | experimental | | [`resource`](https://napplet.run/docs/naps/resource.md) | Sandboxed byte fetching over https, blossom, nostr, and data URIs. | supported | | [`upload`](https://napplet.run/docs/naps/upload.md) | File and blob upload over NIP-96 and Blossom rails. | supported | | [`cvm`](https://napplet.run/docs/naps/cvm.md) | ContextVM bridge — MCP tools and resources over Nostr. | experimental | ### Devices and realtime | Domain | Purpose | Maturity | | --- | --- | --- | | [`gamepad`](https://napplet.run/docs/naps/gamepad.md) | Focus-scoped controller snapshots. | supported | | [`ble`](https://napplet.run/docs/naps/ble.md) | Bluetooth LE / GATT sessions with opaque session ids. | experimental | | [`serial`](https://napplet.run/docs/naps/serial.md) | User-approved serial port sessions. | experimental | | [`webrtc`](https://napplet.run/docs/naps/webrtc.md) | Runtime-owned WebRTC sessions carrying application payloads. | experimental | ### AI | Domain | Purpose | Maturity | | --- | --- | --- | | [`llm`](https://napplet.run/docs/naps/llm.md) | User-selected language model inference through the runtime. | exploratory | ## Implementation maturity These release-time labels describe our shipped implementation, separately from upstream proposal status. Supported does not mean frozen; experimental does not waive conformance requirements. All capabilities remain subject to change. See [maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). | Domain | Implementation maturity | Proposal status | Assessment | | --- | --- | --- | --- | | `shell` | supported | [merged](https://github.com/napplet/naps/pull/101) | Optional readiness and environment binding has focused shim and reference-runtime coverage. | | `relay` | supported | [in-review](https://github.com/napplet/naps/pull/2) | Relay reads, publishing, and subscription lifecycle have maintained shim and integration tests; upstream naming remains under review. | | `identity` | supported | [merged](https://github.com/napplet/naps/pull/102) | Read-only identity queries and updates have focused binding and reference-runtime coverage. | | `storage` | supported | [in-review](https://github.com/napplet/naps/pull/3) | Key-value binding has focused request/result tests and tutorial usage; host storage isolation remains the host responsibility. | | `inc` | supported | [merged](https://github.com/napplet/naps/pull/104) | Topic and channel bindings have compatibility tests and authenticated-endpoint reference-runtime coverage. | | `theme` | supported | [merged](https://github.com/napplet/naps/pull/103) | Theme queries and pushed updates have focused binding and opaque-iframe integration coverage. | | `keys` | experimental | [in-review](https://github.com/napplet/naps/pull/9) | Keyboard binding ships, but dedicated domain regression coverage is limited; retain explicit evaluation opt-in. | | `media` | supported | [in-review](https://github.com/napplet/naps/pull/10) | Session creation and playback binding have focused regression coverage; proposed torrent playback amendments are not implied. | | `notify` | experimental | [in-review](https://github.com/napplet/naps/pull/11) | Notification binding ships with limited dedicated domain regression evidence; broader host integration confidence is unverified. | | `config` | exploratory | [draft](https://github.com/napplet/naps/pull/14) | Configuration binding is tested, but the upstream draft is marked MAY DEFER; use for design exploration. | | `resource` | supported | [in-review](https://github.com/napplet/naps/pull/80) | Resource fetching has focused shim and type tests; the reverted merge and reopened proposal do not change that implementation assessment. | | `cvm` | experimental | [in-review](https://github.com/napplet/naps/pull/31) | Session and registry bindings are tested; upstream explicitly marks the contract NEEDS WORK. | | `outbox` | supported | [in-review](https://github.com/napplet/naps/pull/32) | Routing and subscription bindings have focused tests and an integration regression for structured-clone boundaries. | | `upload` | supported | [in-review](https://github.com/napplet/naps/pull/33) | Upload binding has focused tests for request/result behavior; host rail support remains runtime-specific. | | `intent` | supported | [merged](https://github.com/napplet/naps/pull/91) | Convention normalization and dispatch have focused binding and reference-runtime integration tests. | | `ble` | experimental | [in-review](https://github.com/napplet/naps/pull/62) | GATT binding has unit coverage; real-device behavior across host backends is not established by these tests. | | `webrtc` | experimental | [in-review](https://github.com/napplet/naps/pull/59) | Signaling and session binding have unit coverage; interoperable live peer sessions remain unverified here. | | `link` | supported | [in-review](https://github.com/napplet/naps/pull/53) | The small link-opening binding has focused regression coverage; navigation authorization stays with the host. | | `count` | experimental | [in-review](https://github.com/napplet/naps/pull/69) | Count binding is tested, but accuracy and aggregation across real relay backends remain outside that evidence. | | `lists` | experimental | [in-review](https://github.com/napplet/naps/pull/68) | List mutation binding is tested; concurrent real-host mutation behavior is not established by binding tests. | | `serial` | experimental | [in-review](https://github.com/napplet/naps/pull/61) | Serial binding has unit coverage; device and native-host integration evidence is limited. | | `fs` | experimental | [in-review](https://github.com/napplet/naps/pull/88) | Filesystem binding has MVP coverage; portability and authorization across storage backends remain under evaluation. | | `common` | experimental | [in-review](https://github.com/napplet/naps/pull/67) | Social action binding is tested; broader host integration and evolving action semantics warrant explicit opt-in. | | `dm` | experimental | [draft](https://github.com/napplet/naps/pull/74) | Direct-message binding has shim and type coverage; transport interoperability is not established by those tests. | | `gamepad` | supported | [in-review](https://github.com/napplet/naps/pull/130) | Cross-repository delivery includes binding, security, runtime, and conformance verification; the reopened proposal is still subject to change. | | `llm` | exploratory | [draft](https://github.com/napplet/naps/pull/134) | Early published draft has binding and mock-runtime tests; provider behavior and API design remain exploratory. | ## Core domain union [`@napplet/core`](https://napplet.run/docs/packages/core.md) exports a `NapDomain` string union for the foundational domains — `shell`, `relay`, `identity`, `storage`, `inc`, `theme`, `keys`, `media`, `notify`, `config`, `resource`, `cvm`, `outbox`, `upload`, `intent`, `ble`, `webrtc`, `link`, `count`, `lists`, `serial`, `fs`, `common`, `dm`, `gamepad`, `llm` — used as the discriminant for envelope routing and domain presence. ## Where to go next - [`@napplet/nap`](https://napplet.run/docs/packages/nap.md) — the package and its subpath patterns - [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) — typed helpers and per-domain message unions - [NIP-5D explained](https://napplet.run/docs/guide/nip-5d.md#nap-extension-framework) — the NAP framework --- # INTENT > **Implementation: supported · Proposal: [merged](https://github.com/napplet/naps/pull/91)** (status checked 2026-10-31). Convention normalization and dispatch have focused binding and reference-runtime integration tests. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. ```ts if (window.napplet?.intent) { const { available } = await window.napplet.intent.available('profile'); if (available) { const result = await window.napplet.intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); if (!result.ok) console.error(result.error); } } ``` This non-normative guide defers to the living [NAP-INTENT proposal](https://github.com/napplet/naps/pull/91). Feature-gate with `if (window.napplet?.intent)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/intent`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). ## Delivery and recommendations ```ts import { intent } from '@napplet/sdk'; intent.onDelivery(({ sender, convention, payload }) => { console.log(sender, convention, payload); }); await intent.invoke('napplet:profile/open?pubkey=abc123'); ``` The runtime binding retains deliveries until a listener registers. Discovery candidates expose opaque catalog `id` values and `contracts` with parameter names. [INC](https://napplet.run/docs/naps/inc.md) peers instead use identifiers for running authenticated endpoints. A NAP-INTENT URI may append a bare `#naddr1…` recommendation; the binding sends its coordinate and relay hints as `handlerHint`, outside convention identity and payload. An applicable user default takes precedence. INC URI operations reject fragments. See the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). --- # KEYS > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/9)** (status checked 2026-10-31). Keyboard binding ships, but dedicated domain regression coverage is limited; retain explicit evaluation opt-in. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Keyboard bindings and action registration — the shell binds keys to named actions. ```ts await window.napplet.keys.registerAction({ id: 'editor.save', label: 'Save', defaultKey: 'Ctrl+S' }); const sub = window.napplet.keys.onAction('editor.save', () => save()); ``` This non-normative guide defers to the living [NAP-KEYS proposal](https://github.com/napplet/naps/pull/9). Feature-gate with `if (window.napplet?.keys)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/keys`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # LINK > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/53)** (status checked 2026-10-31). The small link-opening binding has focused regression coverage; navigation authorization stays with the host. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Shell-mediated external link opening. This is user-visible navigation, not byte fetching; the shell owns prompting, policy, opener isolation, and browser context. ```ts if (window.napplet?.link) { const result = await window.napplet.link.open('https://example.com/post/123', { label: 'Read post', }); if (result.status === 'denied') showInlineFallback(); } ``` This non-normative guide defers to the living [NAP-LINK proposal](https://github.com/napplet/naps/pull/53). Feature-gate with `if (window.napplet?.link)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/link`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # LISTS > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/68)** (status checked 2026-10-31). List mutation binding is tested; concurrent real-host mutation behavior is not established by binding tests. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Runtime-mediated NIP-51 list mutations. Napplets send add/remove intent while the runtime owns current-event lookup, kind/type mapping, tag formatting, private item encryption, event preservation, signing, and publishing. ```ts if (window.napplet?.lists) { await window.napplet.lists.add({ type: 'mute-list' }, [ { itemType: 'pubkey', value: 'abc123...' }, ]); } ``` This non-normative guide defers to the living [NAP-LISTS proposal](https://github.com/napplet/naps/pull/68). Feature-gate with `if (window.napplet?.lists)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/lists`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # LLM > **Implementation: exploratory · Proposal: [draft](https://github.com/napplet/naps/pull/134)** (status checked 2026-10-31). Early published draft has binding and mock-runtime tests; provider behavior and API design remain exploratory. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). ## User-selected language models through the runtime This non-normative guide follows the living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134). When the runtime exposes `window.napplet.llm`, a napplet can ask the user to pick a model connection and submit stateless generations against it. The napplet owns its prompts, conversation, and output validation; the runtime owns connection selection, credentials, disclosure consent, and provider access. Domain presence does not imply a configured connection or a consent grant: handle `unsupported` and `user-cancelled` as ordinary outcomes and keep the rest of the napplet usable without inference. Import `llm` from `@napplet/sdk` or `@napplet/sdk/llm`; root exports include prefixed helpers such as `llmSelect` and `llmGenerate`. The dedicated subpath also provides bare aliases such as `select` and `generate`. ```ts import { llm, isLlmFailure, llmText } from '@napplet/sdk/llm'; if (window.napplet?.llm) { const model = await llm.select({ json: true }); const generation = await llm.generate({ handle: model.handle, messages: [ { role: 'system', content: [{ type: 'text', text: 'Return a JSON object with a short label.' }] }, { role: 'user', content: [{ type: 'text', text: note }] }, ], format: 'json', }); if (generation.finish === 'stop') applyLabel(JSON.parse(llmText(generation))); } ``` ## Operations and results | Method | Result | | --- | --- | | `select(requirements?)` | `LlmModel` | | `describe(handle)` | `LlmModel` | | `generate(request, options?)` | `LlmGeneration`, with `requestId` on the pending promise | | `cancel(requestId)` | `boolean` | `select` asks the user to choose a connection meeting the optional `model`, `images`, `json`, and `tools` requirements; a user default that meets them may satisfy it without a chooser. The returned `handle` is opaque, scoped to this endpoint and account, and never a URL or credential. `describe` revalidates a handle without a chooser; a changed connection, model, credential, account, or endpoint lifetime invalidates handles and rejects with `selection-expired`, so select again. `route` is a human-readable disclosure path including intermediaries; "local model" does not promise data stays on the device when the runtime is hosted. `tools` and `toolChoiceModes` say whether the connection calls tools and which of `auto`, `none`, `required`, and `tool` (named forcing) it supports; a request for an unlisted mode fails with `unsupported` instead of being substituted. `generate` is one stateless request: the supplied messages are the entire conversation, and the runtime does not append or drop anything. Optional `maxTokens` (a positive integer the runtime may cap) and `temperature` (unsupported values fail with `unsupported`) tune the request. The terminal `content` is a list of `text`, `thinking`, and `toolCall` parts; `llmText(generation)` concatenates the text parts. Only `finish: "stop"` is normal completion; `length` and `refusal` may carry partial or empty content, carry no tool calls, and must not be presented as complete. With `format: "json"`, the text of a `stop` result parses as a JSON object, but its schema, accuracy, and suitability remain the napplet's checks. `thinking` parts are display-only: the runtime omits them when history is replayed. `usage` is provider-reported token counts (including optional cache and reasoning counts), not a price. ## Tool calls Pass `tools` (portable names matching `^[A-Za-z0-9_-]{1,64}$`, an optional description, and a JSON Schema whose root is `type: "object"`) and an optional `toolChoice`. The model may answer with `finish: "toolUse"` and one or more `toolCall` parts. The runtime never executes them: a call is a request, not authorization, so validate its arguments and obtain any separately required capability before running it. Then send the whole conversation again, with the assistant turn as returned and one `toolResult` message per call immediately after it, reusing the call's `toolCallId` and name. Set `isError: true` when the tool failed. Tool results re-enter the model as untrusted input. ```ts import { llm, llmText, llmToolCalls } from '@napplet/sdk/llm'; import type { LlmMessage, LlmTool } from '@napplet/sdk/llm'; const tools: LlmTool[] = [{ name: 'get_weather', description: 'Look up current weather', parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] }, }]; const messages: LlmMessage[] = [{ role: 'user', content: [{ type: 'text', text: 'What is the weather in Berlin?' }] }]; let generation = await llm.generate({ handle: model.handle, messages, tools }); while (generation.finish === 'toolUse') { messages.push({ role: 'assistant', content: generation.content }); for (const call of llmToolCalls(generation)) { const outcome = await runToolIfAllowed(call); // the napplet's own validation and authority messages.push({ role: 'toolResult', toolCallId: call.toolCallId, toolName: call.name, isError: !outcome.ok, content: [{ type: 'text', text: outcome.text }] }); } generation = await llm.generate({ handle: model.handle, messages, tools }); } if (generation.finish === 'stop') show(llmText(generation)); ``` Each consent prompt discloses tool definitions and results as well as prompts, so every round trip is a new disclosure and may be a new charge. History with completed calls and results may be replayed without `tools`, which continues the exchange without enabling new calls. ## Streaming and cancellation The pending promise returned by `generate` exposes `requestId`. Pass `onDelta` to receive ordered provisional block events and `signal` to map an abort to `llm.cancel`; the generation still settles through its own result, because completion may win the race. Each block (`text`, `thinking`, or `toolCall`, identified by `blockIndex`) streams one `start`, zero or more `delta` events carrying `text` fragments (JSON argument fragments for a tool call), and one `end`; a `toolCall` start carries the call's `toolCallId` and `name`. Deltas are optional, so render provisional output as provisional and replace it with the terminal `content` rather than appending. Never execute a tool from a delta or a block `end`. The binding drops deltas that do not match the schema, stops provisional delivery after a sequence gap, and still resolves the terminal result. ```ts const controller = new AbortController(); const pending = llm.generate(request, { signal: controller.signal, onDelta: (delta) => { if (delta.event === 'delta' && delta.block === 'text') showProvisional(delta.blockIndex, delta.text); }, }); cancelButton.onclick = () => controller.abort(); try { showFinal(llmText(await pending)); } catch (error) { if (isLlmFailure(error) && error.code === 'cancelled') showCancelled(); else throw error; } ``` `cancel(requestId)` resolves `true` only when an active generation owned by this endpoint was terminated; unknown, terminal, or foreign ids resolve `false`. The runtime bounds generation time, including the consent wait, and reports `timeout`. Neither cancellation nor timeout proves the provider stopped computing or charging, so never retry an ambiguous outcome automatically; a user-initiated retry is a new request and a new consent. ## Failures A runtime `Error` record rejects the promise with an `Error` whose `code` is the specification category (`unsupported`, `invalid-request`, `selection-expired`, `user-cancelled`, `cancelled`, `timeout`, `quota-exceeded`, `provider-error`) and whose `message` is a diagnostic for people, not for matching. Unknown codes are still failures. Use `isLlmFailure(error)` to recognize one across bundle boundaries. A lost response is different from an explicit error: the binding's own transport timeout rejects with a plain `Error` and no `code`. Runtime limits are policy, not fixed maxima; a request that exceeds them fails explicitly instead of truncating messages, dropping images, or changing models. ## Declaring the domain Declare `llm` as required (`R`) only when inference is the napplet's core task; otherwise declare it optional (`O`) and check `window.napplet?.llm` before use. The vite plugin infers the bare domain from `@napplet/sdk/llm` and `@napplet/nap/llm` imports and from direct `window.napplet.llm` reads. Declaring the domain configures no connection and grants no consent. Model output is untrusted data: render it as text, never as markup, and never treat it as authorization to execute tools, sign, pay, or fetch. ## Conformance scope Conformance validates the four request schemas, including tool definitions, tool-choice rules, part placement by role, and adjacent tool-result matching, rejects result and delta envelopes emitted by napplets, and runs the reference shell's deterministic mock: one connection (`reference-model`, with images, JSON, tools, and every tool-choice mode), endpoint-scoped handles, exact-model requirement matching, a `start` / `delta` / `end` block lifecycle before the authoritative result, echoed text or JSON output (trailing tool results echoed, with the specified `Tool execution failed.` prefix for errors), a `toolUse` result for `required` or named choices, and fixture markers in the last user text (`[[decline]]` for a declined prompt, `[[refuse]]` for a provider refusal, `[[stall]]` for a generation that stays active until `cancel`, `[[think]]` for a thinking block, `[[tool]]` for a call to the first supplied tool). The mock never executes a tool call. It exercises protocol shape, not live inference, consent prompts, provider adapters, or billing; verify those in an LLM-capable host. --- # NAP maturity and opt-in This is non-normative implementation guidance for the `@napplet/*` packages. The living [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) and [NAP proposals](https://github.com/napplet/naps) govern protocol behavior. These labels are our implementation assessments, not official governance stages, security certifications, or compatibility guarantees. All NAP capabilities remain subject to change. ## Two separate signals **Proposal status** records the upstream state at the date shown in `proposal.checkedAt`: `merged`, `in-review`, `draft` (a GitHub draft PR), or `unknown`. It describes the linked proposal, not every amendment to that domain. A merged document can still call itself a draft. A reverted merge does not make a reopened proposal merged. **Implementation maturity** describes this package's implementation: - **supported**: maintained and tested for ordinary integration; its API is not frozen. - **experimental**: implemented for deliberate evaluation; substantial changes remain likely. - **exploratory**: early implementation intended to explore a published draft. A missing classification is excluded by the selection helper. Deferred or rejected proposals are retired rather than offered as another opt-in tier. Experimental status never waives protocol conformance requirements. ## Inspect installed-package metadata ```ts import { NAP_IMPLEMENTATIONS, getNapImplementation } from '@napplet/core'; const llm = getNapImplementation('llm'); console.log(llm?.maturity, llm?.proposal.url, llm?.proposal.checkedAt); console.log(NAP_IMPLEMENTATIONS.storage.rationale); ``` Metadata is bundled at release time. It makes no runtime GitHub requests and does not identify a remote shell's supported protocol revision. An optional `implementationReference` records an upstream revision, the repository evidence, and the scope of a historical implementation verification. It does not claim that the whole current implementation matches that revision; later changes are outside the recorded scope. Missing provenance stays missing; a fetched head SHA alone is not verification. The linked living proposal remains authoritative, even when a historical verification revision is recorded. ## Select supported domains Supply only domains that the host implements and has authorized for this napplet. The helper filters those candidates; it never discovers host support, grants permission, or adds a domain. ```ts import { selectNapDomains } from '@napplet/core'; import { renderNappletRuntimePreludeCall } from '@napplet/shim/prelude'; const selection = selectNapDomains({ candidates: ['storage', 'identity', 'llm'], }); // selection.domains: ['storage', 'identity'] // selection.excluded explains why llm was omitted. const activate = renderNappletRuntimePreludeCall({ domains: selection.domains }); ``` Inline the maintained `@napplet/shim/prelude.global` artifact followed by `activate` before any napplet scripts run. The [shim guide](https://napplet.run/docs/packages/shim.md) shows the complete injection pattern. ## Opt into one experiment ```ts const selection = selectNapDomains({ candidates: ['storage', 'llm', 'ble'], optIn: ['llm'], }); // ['storage', 'llm']; ble remains excluded. ``` An opt-in is intersected with candidates. Opting into `llm` cannot activate it when the host did not include it. Adding another experimental implementation in a package upgrade does not activate it through this opt-in. `allowedMaturities` defaults to `['supported']`. An explicit empty array allows only named opt-ins. A broader list such as `['supported', 'experimental']` is possible, but admits future candidate domains classified at those levels. Prefer named opt-ins when upgrades must not enable new experiments automatically. Classifications can also change between releases; review catalog changes during upgrades. The selector returns `domains` and `excluded`. Exclusion reasons are `unknown-domain`, `unclassified`, and `maturity-not-allowed`. Inputs are not mutated; results are deduplicated in candidate order. ## Enforce the same decision in the host Shim filtering is not a security boundary: a napplet can send `postMessage` directly. Apply `selection.domains` to the endpoint's host dispatch allowlist as well as namespace injection. Keep source identity checks, per-operation authorization, and domain-specific requirements in place. Selection does not automatically enable dependency domains. The repository's reference mock supports the same pattern: ```ts import { createReferenceShell } from '@napplet/conformance'; const shell = createReferenceShell({ domains: selection.domains }); // Attach this mock only to the authenticated test endpoint. // Both handle() and handleFrom() enforce the host domain ceiling. ``` For browser evaluation, `bootAndCollect` accepts `domains` and an optional `maturity` policy. It resolves that policy before boot and passes the same domains to the prelude and reference dispatcher. Its normal conformance mode keeps all domains enabled so experimental APIs still receive full protocol checks. Local policy exclusion does not turn a valid envelope into a conformance failure. ## Prelude compatibility `@napplet/shim/prelude` has no install-on-import side effect. Its existing explicit `domains` behavior remains unchanged when `maturity` is omitted. Passing `maturity: {}` enables supported-only filtering; `maturity: { optIn: ['llm'] }` adds that named candidate exception. Rendering resolves policy on the host and sends only the selected domain list into the prelude. The legacy root `@napplet/shim` entrypoint still installs every bundled domain on import for compatibility. Do not use it for policy-controlled injection. If it was already imported during host setup, the explicit prelude replaces the namespace with the selected set; it does not retroactively undo earlier initialization or calls. Complete policy selection before napplet code runs and enforce the selected set in host dispatch. ## Required and optional domains This explanation defers to NIP-5D's **Required and Optional Capabilities** and **Runtime Domain Availability** sections. Check the complete manifest `R` set against the domains the shell can expose after policy selection. A missing required domain means the shell should reject loading or warn that full functionality is unavailable. Missing `O` domains alone do not prevent loading. Neither declaration grants access. Napplets check namespace presence and gracefully degrade or stop using absent domains. Maturity is not encoded in the manifest, HTML tags, injected namespace, or wire envelopes. Any interoperable semantic compatibility checks or negotiation belong in the relevant NAP first. ## Maintaining classifications Edit `packages/core/src/nap-catalog.ts` with evidence from implementation tests, real integration, known design uncertainty, and upstream discussion. Verify the linked proposal's current state and update its check date. Do not infer implementation maturity from PR merge state alone. The core type contract and coverage test require metadata for every shipped domain; domain overview tables and page notices render directly from this catalog during docs builds. Promotions and demotions are reviewable package changes. Record why the evidence changed, update tests and release notes, and retire implementations whose proposals are deferred or rejected according to repository policy. No new release channel is required. ### Scheduled upstream checks The **NAP metadata review** GitHub Actions workflow checks upstream daily at 05:43 UTC and supports **Run workflow** on the default branch. It maintains one draft PR on `chore/nap-proposal-refresh`. The schedule starts after the workflow is merged into the default branch. Run the same check locally with `GH_TOKEN=… pnpm refresh:nap-maturity` using a token that can read the public NAP repository. The updater refreshes proposal status and its check date when an observation changes. It detects open proposal head changes, later edits or removals of merged NAP files, and registry status changes. Unchanged daily checks produce no commits, check-date changes, or package bumps; the workflow summary records successful polling. The catalog date therefore records the last committed observation, not the latest cron execution. API errors, missing data, or a proposal changing during collection fail the run before local files are updated. `scripts/data/nap-proposal-observations.json` contains observed heads and file revisions solely for change detection. These revisions are not implementation verification references or normative pins. The updater never changes implementation maturity, rationale, evidence, or `implementationReference`. A closed unmerged PR becomes `unknown` and triggers a retirement review; removals and definite deferred/rejected registry states also receive explicit warnings. Maintainers must investigate and retire dependent surface under AGENTS.md before accepting a retirement change. The bot does not perform that sweep or change runtime availability. Review the generated report in `scripts/data/nap-proposal-review.md`, inspect changed upstream contracts, and decide whether implementation changes or a separate follow-up are needed. Merging an observation PR acknowledges its baseline; it does not certify implementation compatibility. A shipped catalog change includes a core patch changeset. Follow the usual release-metadata procedure before shipping; no bot job merges or publishes packages. Observation-only updates need no package release. Docs render merged catalog changes on the next site build, and installed packages receive them through ordinary releases. Repository Actions settings must allow GitHub Actions to create pull requests. The workflow uses `GITHUB_TOKEN` by default and validates build, types and units itself because PRs created with that token do not trigger the usual PR workflows. To trigger normal PR checks automatically, configure the optional `NAP_MATURITY_BOT_TOKEN` Actions secret with a dedicated fine-grained token that can write repository contents and pull requests. Without it, a maintainer must arrange the required PR checks before merging. If a scheduled run fails, inspect its logs and rerun it after fixing API access or source mapping; no partial upstream read is committed. The fixed bot branch is managed by the updater, so put implementation fixes on a separate branch or pause the workflow while editing that branch. --- # MEDIA > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/10)** (status checked 2026-10-31). Session creation and playback binding have focused regression coverage; proposed torrent playback amendments are not implied. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Ownership-aware media sessions and playback control. ```ts const { sessionId } = await window.napplet.media.createSession({ owner: 'napplet', metadata: { title: 'My Song', artist: 'The Artist' }, }); window.napplet.media.reportState(sessionId, { status: 'playing', position: 0, duration: 240 }); window.napplet.media.onCommand(sessionId, (action) => { if (action === 'pause') player.pause(); }); ``` This non-normative guide defers to the living [NAP-MEDIA proposal](https://github.com/napplet/naps/pull/10). Feature-gate with `if (window.napplet?.media)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/media`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # NOTIFY > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/11)** (status checked 2026-10-31). Notification binding ships with limited dedicated domain regression evidence; broader host integration confidence is unverified. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Shell-rendered notifications, badges, and interaction callbacks. ```ts const { notificationId } = await window.napplet.notify.send({ title: 'New message', body: 'Alice: hey!' }); window.napplet.notify.badge(3); window.napplet.notify.onAction((id, actionId) => { if (actionId === 'reply') openReply(id); }); ``` This non-normative guide defers to the living [NAP-NOTIFY proposal](https://github.com/napplet/naps/pull/11). Feature-gate with `if (window.napplet?.notify)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/notify`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # OUTBOX > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/32)** (status checked 2026-10-31). Routing and subscription bindings have focused tests and an integration regression for structured-clone boundaries. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Outbox-aware relay routing — `getEvent` / `query` / `subscribe` / `publish` / `resolveRelays`; the shell owns NIP-65 relay discovery, fallback, dedup, signature validation, signing, and fanout. This is the default event-read and publish boundary when relay selection is part of result correctness. ```ts if (window.napplet?.outbox) { await window.napplet.outbox.getEvent('ev1…', { author: 'ab12…' }); const { events } = await window.napplet.outbox.query( [{ authors: ['ab12…'], kinds: [1], limit: 20 }], { authors: ['ab12…'], timeoutMs: 3000 }, ); const sub = window.napplet.outbox.subscribe( [{ authors: ['ab12…'], kinds: [1], limit: 20 }], { authors: ['ab12…'], timeoutMs: 3000 }, ); sub.on('event', (result) => render(result.event, result.sidecar?.relayHints)); await window.napplet.outbox.publish({ kind: 1, content: 'gm', tags: [], created_at: Math.floor(Date.now() / 1000), }); } ``` This non-normative guide defers to the living [NAP-OUTBOX proposal](https://github.com/napplet/naps/pull/32). Feature-gate with `if (window.napplet?.outbox)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/outbox`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # RELAY > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/2)** (status checked 2026-10-31). Relay reads, publishing, and subscription lifecycle have maintained shim and integration tests; upstream naming remains under review. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Low-level Nostr relay proxy — subscribe, publish, and one-shot query through the shell's relay pool. Use this only for explicit relay-local behavior such as a group relay, diagnostics, or protocol tooling. For normal social reads and publishes, use [`outbox`](https://napplet.run/docs/naps/outbox.md) or a higher-level domain such as [`common`](https://napplet.run/docs/naps/common.md), [`lists`](https://napplet.run/docs/naps/lists.md), [`count`](https://napplet.run/docs/naps/count.md), or [`dm`](https://napplet.run/docs/naps/dm.md). ```ts // Explicit relay-local subscription (returns a handle with .close()) const sub = window.napplet.relay.subscribe( [{ kinds: [9, 10, 11, 12], limit: 20 }], (result) => render(result.event), () => console.log('end of stored events'), { relay: 'wss://groups.example.com' }, ); ``` This non-normative guide defers to the living [NAP-RELAY proposal](https://github.com/napplet/naps/pull/2). Feature-gate with `if (window.napplet?.relay)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/relay`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # RESOURCE > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/80)** (status checked 2026-10-31). Resource fetching has focused shim and type tests; the reverted merge and reopened proposal do not change that implementation assessment. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Sandboxed byte fetching (`info() -> ResourceInfo`, `bytes(url, options?) -> Blob`, `bytesMany(requests) -> ResourceBytesItem[]`) over https / blossom ([BUD-10](https://github.com/hzrd149/blossom/blob/master/buds/10.md) `blossom:.` URIs) / nostr / data schemes — the only network-fetch primitive available inside the iframe sandbox. ```ts const info = await window.napplet.resource.info(); const blob = await window.napplet.resource.bytes('blossom:.png', { servers: ['https://cdn.hzrd149.com'], }); const items = await window.napplet.resource.bytesMany([ { url: 'https://example.com/avatar.png' }, { url: 'blossom:.png', servers: ['https://cdn.hzrd149.com'] }, ]); // Managed object URL — revoke when done to free memory const { url, revoke } = window.napplet.resource.bytesAsObjectURL('blossom:.png'); imgEl.src = url; imgEl.onload = () => revoke(); ``` This non-normative guide defers to the living [NAP-RESOURCE proposal](https://github.com/napplet/naps/pull/80). Feature-gate with `if (window.napplet?.resource)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/resource`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # SERIAL > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/61)** (status checked 2026-10-31). Serial binding has unit coverage; device and native-host integration evidence is limited. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Runtime-mediated serial device access. A napplet asks the shell to select and open a user-approved serial session, writes byte arrays to it, and receives shell-pushed `state`, `data`, and `closed` events. The shell owns device selection, permissions, raw port handles, streams, OS paths, read loops, and lifecycle policy. ```ts if (window.napplet?.serial) { const { session } = await window.napplet.serial.open({ options: { baudRate: 115200 } }); const sub = window.napplet.serial.onEvent((event) => { if (event.type === 'data') append(event.data); }); await window.napplet.serial.write(session.id, [112, 105, 110, 103, 10]); } ``` This non-normative guide defers to the living [NAP-SERIAL proposal](https://github.com/napplet/naps/pull/61). Feature-gate with `if (window.napplet?.serial)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/serial`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # SHELL > **Implementation: supported · Proposal: [merged](https://github.com/napplet/naps/pull/101)** (status checked 2026-10-31). Optional readiness and environment binding has focused shim and reference-runtime coverage. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). The runtime may expose `shell` for environment information. `shell.supports(domain)` reads current domain object presence, `shell.services` is empty until environment delivery, and `shell.ready()` / `shell.onReady(handler)` use the retained snapshot. Readiness does not gate other domain calls. See the living [NAP-SHELL](https://github.com/napplet/naps/blob/master/naps/NAP-SHELL.md). ```ts if (window.napplet?.shell) { if (window.napplet.shell.supports('storage')) enableDrafts(); const sub = window.napplet.shell.onReady((environment) => { console.log(environment.services); }); } ``` Named helpers `shellSupports`, `shellReady`, and `shellOnReady` are exported from `@napplet/nap/shell`. This non-normative guide defers to the living [NAP-SHELL proposal](https://github.com/napplet/naps/pull/101). Feature-gate with `if (window.napplet?.shell)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/shell`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # STORAGE > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/3)** (status checked 2026-10-31). Key-value binding has focused request/result tests and tutorial usage; host storage isolation remains the host responsibility. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Scoped key-value storage proxied through the shell — isolated per napplet identity. ```ts await window.napplet.storage.setItem('draft', 'hello'); const draft = await window.napplet.storage.getItem('draft'); // string | null const keys = await window.napplet.storage.keys(); ``` This non-normative guide defers to the living [NAP-STORAGE proposal](https://github.com/napplet/naps/pull/3). Feature-gate with `if (window.napplet?.storage)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/storage`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # THEME > **Implementation: supported · Proposal: [merged](https://github.com/napplet/naps/pull/103)** (status checked 2026-10-31). Theme queries and pushed updates have focused binding and opaque-iframe integration coverage. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Read-only shell theme access — colors, fonts, background, and title. The shell owns theming; the napplet reads it and reacts to changes. ```ts const theme = await window.napplet.theme.get(); document.body.style.background = theme.colors.background; const sub = window.napplet.theme.onChanged((t) => applyTheme(t)); ``` This non-normative guide defers to the living [NAP-THEME proposal](https://github.com/napplet/naps/pull/103). Feature-gate with `if (window.napplet?.theme)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/theme`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). ## Updates and fallbacks Theme updates reach napplets with the exposed `theme` domain, independently of manifest requirement declarations. Theme media URLs do not grant network access, so colors and local fonts provide the fallback. See the living [NAP-THEME](https://github.com/napplet/naps/blob/master/naps/NAP-THEME.md). --- # UPLOAD > **Implementation: supported · Proposal: [in-review](https://github.com/napplet/naps/pull/33)** (status checked 2026-10-31). Upload binding has focused tests for request/result behavior; host rail support remains runtime-specific. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Shell-mediated file/blob upload over NIP-96 + Blossom rails; the shell signs auth and returns NIP-94 metadata. ```ts if (window.napplet?.upload) { const info = await window.napplet.upload.info(); const result = await window.napplet.upload.upload({ data: blob, filename: 'pic.png' }); if (result.status === 'complete') attach(result.url, result.nip94); } ``` This non-normative guide defers to the living [NAP-UPLOAD proposal](https://github.com/napplet/naps/pull/33). Feature-gate with `if (window.napplet?.upload)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/upload`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # WEBRTC > **Implementation: experimental · Proposal: [in-review](https://github.com/napplet/naps/pull/59)** (status checked 2026-10-31). Signaling and session binding have unit coverage; interoperable live peer sessions remain unverified here. All capabilities remain subject to change. [Maturity and opt-in policy](https://napplet.run/docs/naps/maturity.md). Runtime-mediated WebRTC sessions. The runtime owns signaling transport, signing and encryption, SDP, ICE, and the `RTCPeerConnection` lifecycle; napplets see only shell-scoped sessions — `direct` to one pubkey or a named `room` — and opaque application payloads delivered as `state`, `peer`, `message`, and `closed` events. ```ts if (window.napplet?.webrtc) { const { session } = await window.napplet.webrtc.open({ scope: { type: 'direct', pubkey: '' } }); const sub = window.napplet.webrtc.onEvent((event) => { if (event.type === 'message') handle(event.from, event.payload); }); await window.napplet.webrtc.send(session.id, { move: 'e4' }); } ``` This non-normative guide defers to the living [NAP-WEBRTC proposal](https://github.com/napplet/naps/pull/59). Feature-gate with `if (window.napplet?.webrtc)` before use; the runtime injects only available domains. Typed helpers ship from [`@napplet/nap/webrtc`](https://napplet.run/docs/packages/nap.md) and [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md). --- # @napplet/boilerplate > Project-only generator behind `napplet create`. `@napplet/boilerplate` is the package-backed generator used by the primary CLI. It clones the [`github.com/napplet/boilerplate`](https://github.com/napplet/boilerplate) template — a Vite + TypeScript napplet starter — and derives the package name from the destination. Deployment name, title, description, and archetypes are owned later by `napplet init`. - **npm:** [`@napplet/boilerplate`](https://www.npmjs.com/package/@napplet/boilerplate) - **Source:** [packages/boilerplate](https://github.com/napplet/web/tree/main/packages/boilerplate) ## Usage ```bash napplet create my-napplet ``` It currently ships one variant, `basic`, but keeps a `--variant` option so future templates can be added without changing the command shape. ## Direct package route The package can still be invoked directly for generator development or custom template testing: ```bash npx @napplet/boilerplate ./my-napplet --yes ``` ## Programmatic CLI API `runCli(argv)` is the import-safe generator entry point used by the standalone CLI. It returns `0` on success or `1` after emitting the same `@napplet/boilerplate:` diagnostic as the package binary. ```ts import { runCli } from '@napplet/boilerplate'; const status = await runCli(['my-napplet', '--template', './template']); ``` ## Options | Option | Purpose | | --- | --- | | `--variant ` | Template variant. Currently `basic`. | | `--template ` | Override the template source (useful for local verification). | | `--yes`, `-y` | Use `./my-napplet` when the destination is omitted. | | `--force` | Allow generation into a non-empty directory. | By default the CLI clones `https://github.com/napplet/boilerplate.git`. ## Deployment metadata boundary The generated project leaves archetype metadata to `napplet init`. Each manifest archetype tag advertises one complete queryless `napplet:/` identity: ```json ["z", "profile"] ["i", "napplet:profile/open"] ``` Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). This non-normative guide defers to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md), [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md), and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). ## See also - [Getting started](https://napplet.run/docs/guide/getting-started.md) — scaffold and run your first napplet - [`@napplet/vite-plugin`](https://napplet.run/docs/packages/vite-plugin.md) — the manifest plugin the template wires up --- # @napplet/cli > Standalone CLI for creating, configuring, inspecting, and deploying napplets. `@napplet/cli` is the command-line deploy and diagnostics tool for built napplets. It creates `.napplet/config.yaml`, discovers built `index.html` artifacts, inspects deploy plans, signs manifest events, uploads files to Blossom servers, publishes to relays, and runs local napplet tooling such as conformance and Paja. The standalone binary runs every built-in command without separately installed Node.js, npm, npx, Deno, Git, Kehto, or browser tooling. Screenshot, conformance, and Paja commands download a private, checksum-verified Node runtime and maintained packages on first use; browser commands also provision Chromium. Linux captures include the browser libraries and fonts. Downloads live in the CLI cache and never install into your project or global PATH. First use of these tools requires network access and a writable cache. Later invocations reuse the downloads. The default cache is `~/Library/Caches/napplet` on macOS, `$XDG_CACHE_HOME/napplet` (or `~/.cache/napplet`) on Linux, and `%LOCALAPPDATA%\napplet` on Windows. Set `NAPPLET_CACHE_DIR` to choose another location; copying a populated cache between machines with the same OS and architecture supports offline tooling. User-configured `conformance.command`, `paja.command`, and app-server commands passed to Paja use the software you explicitly select. The generated Vite project and optional skills.sh CLI still need their own development dependencies. Agent skills are installed separately — see [Agent skills](https://napplet.run/docs/guide/agent-skills.md). - **npm:** [`@napplet/cli`](https://www.npmjs.com/package/@napplet/cli) - **JSR:** [`@napplet/cli`](https://jsr.io/@napplet/cli) - **Source:** [packages/cli](https://github.com/napplet/web/tree/main/packages/cli) ## Install Install a checksum-verified standalone binary without Deno: ```bash # macOS or Linux curl -fsSL https://napplet.run/install.sh | sh ``` ```powershell # Windows PowerShell irm https://napplet.run/install.ps1 | iex ``` The installers download from the [stable CLI release](https://github.com/napplet/web/releases/tag/napplet-cli) and verify the asset against its `SHA256SUMS`. Linux and macOS support x64 and ARM64; Windows supports x64. ### Check the installed version ```sh napplet --version # Also available: napplet -v or napplet version ``` This prints the version of the CLI you actually invoked. If you have both standalone and Deno installations, use `type -a napplet` on macOS/Linux or `Get-Command napplet -All` in PowerShell to check which executable takes precedence. A version command reported as unknown means that installation predates version reporting. ### JSR/Deno alternative ```bash deno install --global \ --allow-read --allow-write --allow-run --allow-env --allow-net \ --name napplet \ jsr:@napplet/cli/cli ``` To refresh an existing JSR installation, rerun the command with `--force --reload` after `--global`. Then check the Deno-installed executable directly (`~/.deno/bin/napplet --version` with Deno's default install location); another `napplet` earlier on `PATH` is unaffected. See [Deno's install reference](https://docs.deno.com/runtime/reference/cli/install/) for custom install locations and upgrade flags. Then open the developer guide or check the command reference: ```bash napplet guide napplet --help ``` The permission set is explicit because deploys need to read build output and config, write temporary deploy state, run local helper commands, read signing environment variables, upload to Blossom servers, publish to relays, and connect to remote signers. ## Quick start Run the same path for every new project: ```bash napplet create my-napplet cd my-napplet napplet init npx skills add napplet/web pnpm install # Ask your agent to build the napplet. pnpm verify napplet deploy --dry-run napplet deploy ``` - `napplet create` delegates to the maintained starter generator without setting deploy metadata. - `napplet init` owns name, title, optional description, canonical archetype conventions, and network targets in `.napplet/config.yaml`. In an interactive terminal it guides setup and shows live suggestions from relays such as `wss://relaypag.es`, and suggests Blossom servers from kind `10063` server-list events. - `npx skills add napplet/web` installs the `napplet-*` agent skills for whichever coding agents you use; it is the skills.sh CLI, not a `napplet` subcommand. - `napplet debug` prints resolved config, discovered napplets, deploy targets, manifest templates, and signing readiness without network writes. Manifest template failures (for example a missing description, or built files a current deploy would drop) are reported in `manifests.error` instead of aborting, and `--format current|legacy` selects the event format inspected. - `napplet deploy --dry-run` builds the same deploy plan and signed manifest events without uploading or publishing. - `napplet deploy` uploads files to configured Blossom servers and publishes signed root, named, and optional snapshot manifest events to configured relays. - When no signer flag or stored signer exists, interactive `napplet deploy` starts the NIP-46 connection flow and stores the paired remote signer when native key storage is available. - `--prompt-sec` reads hidden input until Enter; when `.napplet` names a bunker pubkey/npub, a mismatched prompted signer requires interactive confirmation and fails closed in non-interactive runs. ## Commands ```bash napplet guide napplet create [--template ] [--force] napplet init [--force] [--root] [--source-dir ] [--name ] [--title ] [--description <text>] [--archetype <napplet:archetype/intent>] [--relay <url>] [--server <url>] [--proxy <url>] napplet discover [--config <file>] [--all] napplet debug [--format current|legacy] [--config <file>] [--all] [--root] [--name <dtag>] [--snapshot] [--sec <secret>] napplet deploy [--format current|legacy] [--config <file>] [--all] [--root] [--name <dtag>] [--snapshot] [--sec <secret>] [--prompt-sec] [--zapstore | --no-zapstore] [--no-screenshot] [--screenshot [<preview-url>]] [--proxy <url>] [--dry-run] napplet keys store --name <ref> [--sec <secret> | --prompt-sec] napplet keys connect --name <ref> [--relay <url> ...] [--proxy <url>] [--config <file>] napplet keys use --name <ref> [--config <file>] napplet keys list napplet keys delete --name <ref> napplet keys doctor napplet conformance [--config <file>] [--all] [-- <args>] napplet paja [--config <file>] [-- <args>] ``` ## Layouts For a single napplet repository, discovery checks `sourceDir` and prefers `dist/index.html`, falling back to a top-level `index.html`. For workspaces, set `discover.roots` and use `--all`. Each discovered napplet deploys under its own folder name as the named `d` tag. ## YAML configuration and JSON migration Project settings live in `.napplet/config.yaml`. YAML supports comments and uses the same configuration fields as before. Use `--config path/settings.yaml` or `--config path/settings.yml` for a custom file. Configuration updates preserve comments on unchanged values. When a command needs configuration and finds the old JSON file, it asks before migrating. Answer `y` to continue with YAML; `n` or Enter exits with `Cannot continue without migrating`. Migration retains the exact original bytes in `config.json.bak`, preserves file permissions, and removes the active JSON file only after writing YAML. Conflicting YAML or backup files stop migration without overwriting them. To roll back for an older CLI, move the YAML file aside and restore the JSON backup. CI, redirected input, `--json`, and `-i` / `--non-interactive` do not prompt for migration. Approve conversion explicitly with `--migrate-config`: ```sh napplet init --migrate-config -i napplet deploy --config settings.json --migrate-config --dry-run ``` An explicit legacy `--config settings.json` path resolves to `settings.yaml` after migration, including on later invocations. The original command continues using YAML. Help and version remain available before migration. The `napplet migrate` command handles signed manifest events separately; event files and machine-readable output remain JSON. ## Archetype conventions Pass each `--archetype` option as a queryless convention URI, for example `napplet:profile/open`. The CLI derives the role from the URI. YAML configuration accepts URI strings directly: ```yaml metadata: archetypes: - napplet:profile/open ``` The CLI normalizes each URI to an object such as `{ "slug": "profile", "convention": "napplet:profile/open" }`. Existing object configs remain supported; add `"params": ["pubkey"]` to an object to advertise accepted parameter names. Older role-prefixed CLI inputs remain supported for compatibility. This example emits the independent advertisements: ```json ["z", "profile"] ["i", "napplet:profile/open"] ``` This non-normative guide follows the living [NAP-INTENT manifest catalog contract](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md#manifest-catalog-contract). A convention is eligible for dispatch when its archetype segment matches a `z` role on the same manifest. Metadata convention identities are queryless and fragment-free. Trailing `i` values advertise parameter names, not event-kind restrictions or parameter values. ## See also - [`@napplet/vite-plugin`](https://napplet.run/docs/packages/vite-plugin.md) — emits build-side manifest metadata the CLI preserves during deploy. - [`@napplet/conformance-cli`](https://napplet.run/docs/packages/conformance-cli.md) — the default command behind `napplet conformance`; the standalone CLI provisions it in its private cache without a host package manager or global conformance executable. - [Getting started](https://napplet.run/docs/guide/getting-started.md) — scaffold, build, and verify a napplet before deploying. ## Current and legacy event formats From CLI **0.7.0**, deployments default to the current NIP-5D artifact-hash schema. Interactive deploys ask for `current` or `legacy`; scripts can choose with `--format current|legacy`. A non-empty description is required for current event content. Legacy output remains isolated in a temporary serializer; 0.6.x and earlier defaulted to legacy events. Current deploys upload only `/index.html` plus the blobs referenced by `icon`, `screenshot` and `preview` tags; any other built file in the deploy directory makes deploy fail with the list of those files (build a single-file artifact or use `--format legacy`). When config metadata or the Vite plugin sidecar lists a domain as both required and optional, it is emitted only as `O`, matching the Vite plugin. ```sh napplet deploy --dry-run --format current napplet deploy --format legacy napplet migrate signed-event.json --optional theme --output migration-preview.json ``` `migrate` verifies the source signature and writes an unsigned preview with provenance. It does not publish or modify the artifact, and refuses to overwrite an existing output file. Review description, required/optional choices and pointers before signing. See the [migration guide](https://napplet.run/docs/guide/event-migration) for format differences and shell data/consent implications. ## Repository source metadata For current-format deploys, the CLI adds one `source` tag from the Git `origin` of the repository containing each selected build when neither config, the Vite sidecar, nor HTML already supplies it. Nested builds and Git worktrees are supported. HTTPS, HTTP, `ssh://`, and scp-style clone URLs are converted to HTTP(S) repository URLs; inferred URLs omit credentials, query strings, fragments, and the `.git` suffix. SSH remotes use HTTPS without the SSH port. Git is queried locally; no remote is contacted. If Git, a repository, or a usable origin is unavailable, the tag is omitted. Other remotes are not selected, and SSH host aliases are not expanded; configure their public source URL explicitly. Set `metadata.source` in `.napplet/config.yaml` to override every inferred or embedded source: ```yaml metadata: source: https://github.com/example/my-napplet ``` Use `source: false` in that object to omit the tag, including any sidecar or HTML source. A source override must be one absolute HTTP(S) URL without credentials or a NIP-34 `nostr://` clone URL; arrays are not accepted. The precedence is config → sidecar → HTML → Git origin. Root, named, and companion snapshot events retain the resolved source. This follows the current [NIP-5D manifest table](https://github.com/dskvr/nips/blob/nip/5d/5D.md#manifest), which permits zero or one `source` tag. Automatic Git inference does not apply to `--format legacy`; an explicit config override still does. These are CLI defaults, not additional protocol requirements. [NIP-5D §Source](https://github.com/nostr-protocol/nips/pull/2303) also allows one `source-commit` pin and one `source-archive`. Set them explicitly; the CLI never infers a commit, because a dirty or unpushed tree would pin a revision nobody can fetch: ```yaml metadata: source: https://github.com/example/my-napplet sourceCommit: 0123456789abcdef0123456789abcdef01234567 sourceArchive: url: https://dl.example.com/my-napplet-src.tar.gz sha256: c0ffee… mimeType: application/gzip ``` Config values replace matching sidecar tags. `sourceCommit` must be a full lowercase-hex Git commit ID (40 or 64 characters); `sourceArchive.url` must be an absolute HTTPS URL without credentials or a fragment, with the archive's sha256 and MIME type. Deploy refuses malformed or repeated pins rather than signing tags clients ignore. A `source-commit` is only meaningful next to a repository `source`. ## Standalone HTML recovery The CLI reads the head mappings from [NIP-5D, HTML Metadata for Publishing](https://github.com/nostr-protocol/nips/pull/2303), including title, description, named identifier, roles, intents with advertised parameters, required/optional domains, source, server hints, and supported PNG/JPEG/WebP data-URL icons. This is non-normative implementation guidance; the living specification remains authoritative. Embedded metadata is optional, remains untrusted publishing input, and does not replace signed manifest verification at runtime. For current-format deployment, explicit config metadata overrides matching sidecar categories, which override matching head categories. Missing categories fall back to HTML. Explicit target/name selection and configured names override `napplet-id`; a single-project named deploy without a configured name uses the sidecar identifier, then the head identifier, before the existing `default` fallback. Monorepo traversal retains folder-based naming and filtering. Configured Blossom servers override embedded hints; when none are configured, recovered server hints supply upload destinations. The CLI validates selected destinations before manifest signing or upload: each must be a non-empty HTTP(S) origin without credentials, a path, a query, or a fragment; a trailing slash is accepted. Invalid destinations produce an error identifying their config or manifest-hint position. Relay and signing configuration are still needed for publication. Signed or unsigned sidecars can contain additional Nostr tags. Deploy preserves their full string payloads, order, and repeated values through signing and companion snapshots; migration preserves them too. Config overrides and CLI-managed publication fields still apply, but tag names are not restricted to a napplet allowlist. A current event's extra `path`, `description`, `requires`, or `archetype` tag is not a legacy publishing instruction. Extra `description` tags remain separate from event content. These are implementation notes; [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) and the living [NIP-5D proposal](https://github.com/nostr-protocol/nips/pull/2303) define the event format. Root and named events omit lineage tags. Companion snapshots retain the CLI's selected source address as their parent; head provenance does not replace that explicit selection. Root/snapshot events omit the named identifier. Deployment does not rewrite the HTML when config or selection overrides its hints: it hashes and uploads the original bytes and signs the resolved metadata separately. Decoded icon bytes are hashed and uploaded unchanged alongside the HTML, even when `index.html` is the only file in the deployment directory. No extracted icon file is written. If a sidecar overrides the icon, embedded bytes are used only when both hash and MIME type match that declaration. Remote, unsupported, empty, malformed, and MIME-mismatched icon URLs are ignored. Hash-only sidecars still require the matching blob to be supplied separately. Consumers verify decoding before display according to NIP-5D. Legacy-format deployment retains its previous title/description HTML fallback behavior. Deployment manifests (current and legacy, including snapshots) and optional Zapstore application events include `["client", "napplet.run"]` before signing. This identifies the publisher using the [NIP-89 client tag](https://github.com/nostr-protocol/nips/blob/master/89.md#client-tag) and does not change artifact hashes. Dry-run templates include the same attribution. ## Presentation media [NIP-5D §Presentation media](https://github.com/nostr-protocol/nips/pull/2303) describes screenshots and one preview video on the manifest itself, each by the sha256 of its bytes with optional `m`, `dim`, `size`, `alt` and (for the preview) `duration` fields. Current-format deploys handle them in three ways: - Sidecar or configured `screenshot` / `preview` tags whose `x` hash matches a built file are uploaded with `/index.html` (the Vite plugin writes supplied media to `dist/<sha256>.<ext>`). Entries clients would ignore (no usable `x`, an undecodable `m`, more than one `preview`) fail the deploy instead of being signed. - `metadata.screenshots` and `metadata.preview` publish local files kept outside the build directory. Paths resolve relative to the working directory, are hashed, sized, format-sniffed (PNG, JPEG, WebP; WebM or MP4 for the preview) and measured, and replace any sidecar entries of the same kind; an empty list clears them. - Automatic deploy capture (below) adds its PNG as a `screenshot` tag unless `metadata.screenshots` is configured. ```yaml metadata: screenshots: - shots/board.png - path: shots/editor.webp alt: Editing a note preview: path: shots/demo.webm duration: 8.5 ``` `migrate` drops unusable screenshot and preview metadata from a converted event and says so in its notes. These are CLI behaviors, not protocol requirements; clients verify hashes and decoding before display. ## Screenshots and Zapstore metadata Capture a built napplet without starting a host. The command starts a temporary local Kehto Paja preview, captures its iframe, and closes the preview afterwards. The CLI provisions its private runtime and Chromium automatically on first use, without host Node.js or npx. Screenshot and default conformance commands select the tested `@napplet/conformance-cli@0.3.3` runner explicitly, so an older project-local conformance dependency cannot intercept them. Explicit custom conformance commands remain supported. The preview uses Paja’s development identity, fresh browser storage, and live relays, so content may differ from your signed-in shell. ```sh napplet screenshot --output preview.png # Optional: choose another project or an existing shell. napplet screenshot ./my-project --output other.png # Optional: select one iframe and wait for content inside it. napplet screenshot http://localhost:5173 --output ready.png --selector '#napplet-frame' --ready-selector '.app-ready' ``` The default capture is 1200 × 750 pixels with a 1500 ms delay. Override these with `--width`, `--height`, and `--delay` (milliseconds). The command captures only the iframe, waits for fonts, closes Chromium on success or failure, and refuses to overwrite an existing file. Keep screenshots outside the build directory. To customize the application listing, add a `zapstore` object to `.napplet/config.yaml`: ```yaml zapstore: id: org.example.notes name: Notes description: Private notes in a napplet shell. summary: A simple notebook images: - ./preview.png - https://example.org/notes-mobile.png icon: ./assets/icon.png website: https://example.org/notes repository: https://github.com/example/notes license: MIT tags: - notes - productivity ``` The `zapstore` object is optional. When omitted, deploy derives an application ID, name, and description from the selected manifest; a multi-build deploy uses the project directory name for its shared listing. Set `id` and `name` here to control that stable listing identity. Reusing an ID updates the listing under the signing pubkey. Configured fields take precedence over inferred values, and this configuration describes one application listing per deploy invocation. ```sh napplet deploy --dry-run --json napplet deploy # Publish metadata without capturing a new screenshot: napplet deploy --no-screenshot # Skip both application metadata and automatic capture: napplet deploy --no-zapstore ``` Application publication and screenshot capture default to on. `--no-screenshot` skips capture but still publishes the application event, including configured images. `--no-zapstore` skips the application event and automatic capture. `enabled: false` in the `zapstore` object also disables both; `--zapstore` overrides that setting for one deployment. Supplying both Zapstore flags or combining `--no-screenshot` with explicit screenshot options is an error. Dry runs capture a preview and show the application template and resolved media URLs without uploading or publishing. Local PNG, JPEG and WebP images are resolved relative to the working directory, hashed, and uploaded to the configured Blossom servers. Existing HTTP(S) image URLs are used as supplied. Local media URLs point to the first selected Blossom server; if those uploads fail, application publication is skipped and deploy exits nonzero even if another mirror succeeds. The application uses the same signer and relays as the deployment. A requested application event must be accepted by at least one relay for deploy to succeed. The listing uses kind `32267` from the [Software Applications proposal](https://github.com/nostr-protocol/nips/pull/1336), with screenshot URLs in `image` tags. It is published as a separate event before the napplet manifests. The current [NIP-5D manifest](https://github.com/nostr-protocol/nips/pull/2303) does not define an application reference tag, so deploy leaves manifest tags and artifact hashes unchanged. This publishes application metadata only, without software release or asset events; acceptance into a particular store's catalog depends on that store. Deploy starts a local preview for each distinct selected build, captures a temporary PNG, appends it to the application images for that deployment, and adds the same capture to that build's current-format manifests as a NIP-5D `screenshot` tag (`x`, `m image/png`, `dim`, `size`) so the blob is uploaded once and referenced by hash. Configured `metadata.screenshots` keep their own list instead of the capture. Capture failure stops deployment before any upload or relay publication. The temporary image is removed after success or failure. Capture diagnostics go to stderr so `--json` stdout remains valid JSON. No host URL is required. Root and snapshot selections of the same artifact share one capture. To capture an existing shell instead, use `--screenshot <preview-url>`. Use `--screenshot-selector`, `--screenshot-ready-selector`, `--screenshot-width`, `--screenshot-height`, and `--screenshot-delay` for the corresponding capture options. Use the standalone `napplet screenshot` command when you want to keep a PNG on disk. ## Proxies `deploy`, `init` and `keys connect` accept `--proxy <url>` to send their Blossom server and relay traffic through an HTTP or SOCKS5 proxy. Supported schemes are `http://`, `https://`, `socks5://` and `socks5h://`; embedded credentials (`socks5://user:pass@host:port`) are never printed. Use `socks5h://` to resolve host names on the proxy, as Tor requires. ```sh napplet deploy --proxy socks5h://127.0.0.1:9050 napplet keys connect --name remote --proxy http://proxy.example:8080 ``` The proxy carries artifact and media uploads, manifest and application-metadata publishing, `init` suggestions, and remote-signer (NIP-46) relay sessions with their relay information documents. Managed tools started as separate processes, such as screenshot capture, conformance and `paja`, are not proxied by it. Other commands reject the flag. This is CLI transport configuration; it does not change what is published. ## Key storage without system helpers Native keychains are used when available. Otherwise, key commands use password-encrypted local files in `~/.napplet/keys` (AES-256-GCM, PBKDF2-SHA256 with 600,000 iterations). Enter a password of at least 12 characters at the hidden prompt, or set `NAPPLET_KEYSTORE_PASSWORD` for non-interactive use. The password is not stored; reuse it to unlock your keys. Set `NAPPLET_KEYSTORE=portable` to select this store explicitly and `NAPPLET_KEYSTORE_DIR` to change its location. `napplet keys doctor` reports the selected provider. ## Interactive terminals and CI Human terminal sessions show the napplet banner, grouped help, colored deployment summaries, and live upload/publish progress with filled `█` blocks and a `░` remaining track. `napplet deploy` prompts for missing configuration values before signing, preserves existing values, and offers to save completed settings. Use `napplet deploy --dry-run` to preview without uploading or publishing. Use `-i` or `--non-interactive` on any command to disable prompts. CI and redirected streams do this automatically. Missing required values produce actionable errors; supply them through configuration or `--source-dir`, `--description`, `--relay`, and `--server`. `--json` keeps deploy stdout machine-readable, and `NO_COLOR=1` or `--no-color` disables colors. ```sh napplet deploy -i --format current --json napplet init -i --name notes --description "Private notes" ``` Signing secrets remain hidden and are never saved in project configuration. `--prompt-sec` can consume explicitly piped input in automation. Set `NAPPLET_KEYSTORE_PASSWORD` for encrypted local keys; `keys connect` requires interactive remote approval, so CI should use an existing key or session. --- <!-- https://napplet.run/docs/packages/conformance-cli.md --> # @napplet/conformance-cli > The headless `napplet-conformance` runner — drives the > [`@napplet/conformance`](https://napplet.run/docs/packages/conformance.md) engine against a napplet in real > headless Chromium (via Playwright). `@napplet/conformance-cli` lets a napplet prove it conforms to NIP-5D + the NAP protocol **before** publishing — locally and in CI. It is a **development/testing tool**, run from the command line; it is never loaded inside the napplet sandbox. - **npm:** [`@napplet/conformance-cli`](https://www.npmjs.com/package/@napplet/conformance-cli) - **Source:** [packages/conformance-cli](https://github.com/napplet/web/tree/main/packages/conformance-cli) ## Usage ```bash # Build your napplet first, then point the runner at the output: npx napplet-conformance ./dist # or a directory containing index.html / a dist/ subdir: npx napplet-conformance . # or a remotely-served napplet: npx napplet-conformance --url https://my.napplet.example/ ``` ## Wire it up as `test:conformance` ```jsonc { "scripts": { "test:conformance": "napplet-conformance ./dist", "test:conformance:ui": "napplet-conformance --ui . --exec \"vite build --watch\"" } } ``` ```bash pnpm test:conformance # npm / yarn / bun all work — the bin is PM-agnostic ``` ## UI / watch mode (`--ui`) — like `vitest --ui` ```bash napplet-conformance --ui . --exec "vite build --watch" ``` `--ui` serves the standalone conformance web runtime (bundled with this package) plus the napplet, opens your browser, and **re-runs conformance live every time the napplet changes**. The optional `--exec` runs your build in watch mode so source edits rebuild and re-check automatically. ## See also - [`@napplet/conformance`](https://napplet.run/docs/packages/conformance.md) — the engine this CLI drives - [Getting started](https://napplet.run/docs/guide/getting-started.md) — scaffold, build, and verify a napplet - [`@napplet/vite-plugin`](https://napplet.run/docs/packages/vite-plugin.md) — generates the manifest the runner validates The browser runner also supports `npx @napplet/conformance-cli@0.3.3 screenshot [directory|preview-url] --output preview.png`, exposed as `napplet screenshot` by the standalone CLI. See [screenshots and application metadata](https://napplet.run/docs/packages/cli.md#screenshots-and-zapstore-metadata) for browser setup, iframe selection, and publication options. Conformance CLI 0.3.4 requires the published Paja 0.21.x line for GAMEPAD screenshots and compatibility with core/NAP 0.34.0. Paja 0.20.x is excluded because its peer range does not accept those core/NAP versions. Publish this package after its core/NAP dependencies; the standalone CLI runner update follows conformance CLI publication. ## Signed manifests and runtime bindings Headless runs accept `--manifest ./signed-event.json` alongside a local directory or `--url`. Supply the signed Nostr event itself, not an unsigned build sidecar. The CLI checks its structure, signature, event id and exact raw HTML SHA-256 before starting the browser. It boots the same loaded snapshot in both passes, so a changed response cannot replace the verified artifact. Missing or mismatched manifest inputs exit with code 2; omitting the option leaves manifest checks skipped. UI mode rejects this headless-only option; use a NIP-5D pointer in the web runtime instead. ```sh napplet-conformance ./dist --manifest ./signed-event.json ``` The CLI and web runtime inject the maintained `@napplet/shim` bindings before subject code. Installation is observed separately from app errors, and private harness observations are excluded from the envelope log. The normal sandbox remains `allow-scripts`; `--allow-same-origin` is a debug setting and correctly fails the sandbox conformance check. Neither domain presence nor manifest declarations grant production permissions. This is implementation guidance; the living [NIP-5D proposal](https://github.com/nostr-protocol/nips/pull/2303) and [NAPs](https://github.com/napplet/naps) define the protocol. --- <!-- https://napplet.run/docs/packages/conformance-web.md --> # @napplet/conformance-web > Browser conformance runtime for live napplet protocol testing. `@napplet/conformance-web` is the single-window web runtime that powers the deployed [`/conformance`](https://napplet.run/conformance/) app and the `napplet-conformance --ui` watch mode. Point it at a NIP-19 `nevent` or `naddr` for a NIP-5D napplet manifest event and it runs the [`@napplet/conformance`](https://napplet.run/docs/packages/conformance.md) engine live in the browser, then renders a per-check pass/fail tree, the recorded envelope log, and a manifest inspector. - **npm:** [`@napplet/conformance-web`](https://www.npmjs.com/package/@napplet/conformance-web) - **Source:** [apps/conformance](https://github.com/napplet/web/tree/main/apps/conformance) ## Use the hosted app Open the browser runtime at: ```text https://napplet.run/conformance/ ``` Enter a napplet `nevent` or `naddr`, or deep-link with `?target=...`. The hosted runtime uses the pointer's relay hints to resolve the signed manifest event, verifies that it is a napplet event kind (`5129`, `15129`, or `35129`), fetches `/index.html` from the manifest's Blossom `server` hints, verifies the blob hash, and then runs conformance against the verified HTML. ## Run through the CLI Most local workflows should use the Playwright-backed CLI wrapper: ```bash napplet-conformance --ui . --exec "vite build --watch" ``` `--ui` serves this web runtime plus the napplet under test, opens your browser, and re-runs conformance every time the served napplet changes. ## Local development From this repository: ```bash pnpm --filter @napplet/conformance-web dev ``` The app loads the napplet into a `sandbox="allow-scripts"` iframe, attaches a reference shell, records every emitted envelope, and runs the same checks as the headless runner. HTTP URLs remain available in this app for local development and legacy `?url=...` links, but URL mode cannot prove signed NIP-5D manifest-event checks. > **TIP** > > For CI, use [`@napplet/conformance-cli`](https://napplet.run/docs/packages/conformance-cli.md). It serves the > napplet with the right local headers and exits non-zero on failures. ## See also - [`@napplet/conformance`](https://napplet.run/docs/packages/conformance.md) — the engine this runtime executes. - [`@napplet/conformance-cli`](https://napplet.run/docs/packages/conformance-cli.md) — the headless and UI/watch runner that bundles this runtime. --- <!-- https://napplet.run/docs/packages/conformance.md --> # @napplet/conformance > Framework-agnostic conformance engine for the napplet protocol — lets a napplet > self-verify that it conforms to NIP-5D + the NAP wire protocol **before** > publishing. `@napplet/conformance` is a **development/testing tool**. It is not loaded inside the napplet sandbox at runtime. One engine drives two scopes: - **Headless / CI** — via the [`@napplet/conformance-cli`](https://napplet.run/docs/packages/conformance-cli.md) `napplet-conformance` Playwright runner. - **Standalone web runtime** — the single-window `apps/conformance` app. v1 is **zero-config protocol conformance**: NIP-5D manifest-event validity, boots under `sandbox="allow-scripts"`, receives a runtime-injected `window.napplet`, every emitted postMessage envelope validates against the per-NAP validators, graceful degradation when a domain is absent, and no forbidden globals. The harness acts as a NIP-5D shell for the [port-bound identity](https://github.com/nostr-protocol/nips/pull/2303) model: it binds the `MessagePort` from the first valid `napplet.hello`, answers only over that port, and revokes the napplet and removes its frame on a second hello, any other window-level message, a second `load` or the port closing. `wire/port-only` fails a napplet that posts messages at window level instead of through `window.napplet`; `lifecycle/keeps-verified-document` is an advisory for a napplet that navigates or reloads its own frame. `attachReferenceShell(shell, { host, expectedSource })` and `bindNappletPort` give custom harnesses the same binding. - **npm:** [`@napplet/conformance`](https://www.npmjs.com/package/@napplet/conformance) - **JSR:** [`@napplet/conformance`](https://jsr.io/@napplet/conformance) - **Source:** [packages/conformance](https://github.com/napplet/web/tree/main/packages/conformance) ## Local maturity policy The reference mock can enforce a resolved host domain ceiling: `createReferenceShell({ domains: selection.domains })`, where `selection` comes from `selectNapDomains` in `@napplet/core`. The same set belongs in the injected prelude. Per-endpoint grants can narrow this ceiling but cannot widen it; excluded requests remain recorded for envelope validation and receive no response. `bootAndCollect({ ...options, domains: ['storage', 'llm'], maturity: {} })` selects supported candidates before boot and uses that set in both injection and dispatch. A named opt-in such as `maturity: { optIn: ['llm'] }` enables evaluation of that candidate. Normal conformance runs omit maturity filtering and retain full-domain coverage. Experimental status never waives protocol validation. See the [maturity guide](https://napplet.run/docs/naps/maturity.html). ## Install ```bash npm install -D @napplet/conformance ``` ## What's in the box - **`validateEnvelope(msg)`** — runtime validation of any `domain.action` envelope a napplet emits, across every NAP domain. Catches malformed payloads, unknown types, and napplets that put shell→napplet (inbound) traffic on the wire. - **`validateManifestEvent(event)`** — checks that a resolved Nostr event is a NIP-5D napplet manifest (`5129`, `15129`, or `35129`) with a single `/index.html` artifact `x` hash, non-empty description, and bare `R`/`O` domains. Domains outside the toolchain registry produce an advisory `manifest/requires-known` warning without failing the run or implying runtime support. - **`validateManifest(html)`** — compatibility wrapper for older HTML-only harnesses. HTML alone cannot prove a signed NIP-5D manifest event. - **`parsePresentationMedia(event)`** — applies NIP-5D §Presentation media to `screenshot` and `preview` tags: splits NIP-92 `key value` fields, requires one `x` sha256, drops entries clients must ignore, and returns the usable list in manifest order with advisories for everything ignored. - **Reference mock shell**, **check registry**, and **reporters** that the CLI and the web runtime share. The validator surface is kept in lockstep with `@napplet/nap` by a drift test, so a new NAP message type cannot ship without matching conformance coverage. Manifest validation accepts extra Nostr tags and leaves their payloads and multiplicity unchanged. It checks required fields and recognized optional metadata rather than enforcing a tag-name allowlist. A `source` tag is checked for its single-location shape and supported HTTP(S), SSH, Git, scp-like, or NIP-34 `nostr://` format; `source-commit` and `source-archive` are checked for their single-tag shape. Invalid icons, screenshots and previews, and a `source-commit` without a repository `source`, remain advisory (`manifest/media-usable`) with fallback. Extra tags cannot make missing content, invalid hashes, or malformed capability declarations pass. This is non-normative implementation guidance; consult the living [NIP-5D proposal](https://github.com/nostr-protocol/nips/pull/2303) for the requirements. ```ts import { validateEnvelope, validateManifestEvent } from '@napplet/conformance'; validateEnvelope({ type: 'outbox.query', id: 'a', filters: [{ kinds: [1] }], }).ok; // true validateManifestEvent(resolvedManifestEvent).ok; // true when the manifest event is well-formed ``` > **TIP** > > NIP-5D loads a napplet as a single self-contained `/index.html` via > `iframe.srcdoc` (opaque origin), so a napplet's JS is **inline by design**. > Conformance does not penalize inline `<script>` elements. ## See also - [`@napplet/conformance-cli`](https://napplet.run/docs/packages/conformance-cli.md) — the headless runner that drives this engine - [NIP-5D explained](https://napplet.run/docs/guide/nip-5d.md) — manifest & NAP negotiation - [Core concepts](https://napplet.run/docs/guide/concepts.md) — the envelope wire format these validators check ## Custom browser hosts `bootAndCollect({ url, html, runtimePreludeSource })` requires the maintained shim IIFE source and accepts an optional loaded HTML snapshot. The CLI reads `@napplet/shim/prelude.global`; Vite hosts can import it with `?raw`. The harness appends the explicit domain allowlist call, so the napplet sees exactly the bindings the maintained runtime installs; it ships no bindings of its own. Supplying `html` avoids refetching an artifact after the host verifies its signature and raw bytes. The engine remains browser-safe and does not import Node APIs or bundled asset loaders. `installedGlobal` records observed installation independently from `bootError`; an app failure does not imply injection failed. Signature/hash verification belongs to the loading host, while `validateManifestEvent` checks the event structure. This is non-normative implementation guidance; consult the living [NIP-5D proposal](https://github.com/nostr-protocol/nips/pull/2303) and [NAPs](https://github.com/napplet/naps). ## LLM conformance scope NAP-LLM validation checks the four request schemas (exact API-row parameters, strict input records without unknown fields or `null`, enum roles and formats, padded base64 image data, nonempty ids, positive integer `maxTokens`, portable unique tool names with object-rooted schemas, an explicit `toolChoice` only with tools and a listed forced name, `thinking` and `toolCall` parts only in assistant messages, and each tool call answered exactly once by an adjacent, name-matching `toolResult`) and rejects result and delta envelopes emitted by napplets, following the living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134). The reference shell is a deterministic mock: one connection that supports images, JSON, tools, and every tool-choice mode, endpoint-scoped handles, exact-model requirement matching, a `start` / `delta` / `end` block lifecycle before the authoritative result, echoed output (trailing tool results echoed with the specified `Tool execution failed.` prefix for errors), a `toolUse` result for `required` or named choices, and fixture markers in the last user text (`[[decline]]`, `[[refuse]]`, `[[stall]]`, `[[think]]`, `[[tool]]`) for a declined prompt, a provider refusal, a generation that stays active until `cancel`, a display-only thinking block, and a call to the first supplied tool. It never executes a tool call. It returns typed failures rather than fabricated success. A malformed LLM request that still carries a non-empty `id` gets its `.result` with an `invalid-request` error, as the Wire Protocol requires; uncorrelated envelopes and napplet-emitted results are discarded. Passing mock conformance does not prove live inference, consent prompts, provider adapters, or billing behavior; test those in an LLM-capable host. --- <!-- https://napplet.run/docs/packages/core.md --> # @napplet/core > JSON envelope types and NAP dispatch infrastructure for the napplet ecosystem. `@napplet/core` is the single source of truth for all protocol-level definitions. Every other `@napplet/*` package imports its envelope types, dispatch infrastructure, and protocol constants from here. It has **zero dependencies** and no DOM or browser APIs — it works in any JavaScript runtime. - **npm:** [`@napplet/core`](https://www.npmjs.com/package/@napplet/core) - **JSR:** [`@napplet/core`](https://jsr.io/@napplet/core) - **Source:** [packages/core](https://github.com/napplet/web/tree/main/packages/core) ## Implementation maturity NAP implementation maturity and upstream proposal status are separate release-time signals. All capabilities remain subject to change; supported does not mean frozen, and experimental does not waive conformance requirements. Read the [maturity and opt-in guide](https://napplet.run/docs/naps/maturity.html). ```ts import { NAP_IMPLEMENTATIONS, selectNapDomains } from '@napplet/core'; console.log(NAP_IMPLEMENTATIONS.llm.maturity); // exploratory const selection = selectNapDomains({ candidates: ['storage', 'llm', 'ble'], // already implemented and authorized by the host optIn: ['llm'], }); // selection.domains: ['storage', 'llm']; selection.excluded explains omissions. ``` Metadata describes the installed package, not a remote shell's protocol revision. Use the selected domains for both runtime injection and host dispatch. Unknown or unclassified candidates remain excluded, including explicit opt-ins. The catalog does not grant permissions or implement missing host support. ## Install ```bash npm install @napplet/core ``` ## Key exports ```ts import { type NappletMessage, type NapDomain, type NappletGlobal, type NapHandler, type NapDispatch, type IntentOpenOptions, type IntentRequest, type IntentResult, NAP_DOMAINS, SHELL_BRIDGE_URI, PROTOCOL_VERSION, createDispatch, registerNap, dispatch, getRegisteredDomains, ALL_CAPABILITIES, TOPICS, } from '@napplet/core'; ``` ### Envelope types - **`NappletMessage`** — base interface for every message: `{ type: string }` in `domain.action` format. Concrete message types extend it with payload fields. - **`NapDomain`** — string literal union of the NAP capability domains (`'relay' | 'identity' | 'storage' | 'inc' | 'theme' | 'keys' | 'media' | 'notify' | 'config' | 'resource' | 'cvm' | 'outbox' | 'upload' | 'intent' | 'ble' | 'webrtc' | 'link' | 'count' | 'lists' | 'serial' | 'fs' | 'common' | 'dm' | 'shell' | 'gamepad' | 'llm'`). - **`NappletGlobal`** — the runtime-injected `window.napplet` namespace with optional domain properties. - **`NAP_DOMAINS`** — runtime constant array of all domain strings, for iteration and validation. ### Dispatch infrastructure NAP modules self-register at import time; inbound messages route by the domain prefix of `message.type` (the part before the first `.`). - **`createDispatch()`** — factory returning an isolated `{ registerNap, dispatch, getRegisteredDomains }` backed by its own registry. - **`registerNap(domain, handler)`** — register a handler on the module-level singleton registry (throws if the domain is already registered). - **`dispatch(message)`** — route a message to its handler; returns `true` if one was found and called, `false` for an unknown or malformed type. - **`getRegisteredDomains()`** — list registered domain strings. ### Boundary helpers (clone-safety) Every NAP shim crosses the napplet ⇄ shell boundary by structured-cloning a JSON envelope through `postMessage`. Framework reactive values — Svelte 5 `$state`, Vue `reactive`, Solid stores — are `Proxy` objects that are **not** structured- cloneable, so a naive `postMessage` throws `DataCloneError`, which is silently swallowed in async paths (the envelope never crosses). These helpers fix that. - **`sendEnvelope(target, message, targetOrigin?)`** — the single boundary chokepoint the shims post through. Per the active clone mode it posts the envelope, recovers a non-cloneable argument via a snapshot, or throws a loud, actionable, synchronous error instead of swallowing it. - **`toCloneableSnapshot(value)`** — deep snapshot that strips reactive proxies into plain objects/arrays while **preserving binary** (`Uint8Array`, `ArrayBuffer`), `Date`, `RegExp`, `Map`, `Set`, and cycles. Unlike a `JSON` round-trip it is lossless for binary; functions/symbols throw (never masked). - **`setCloneMode(mode)`** / **`getCloneMode()`** — choose how arguments are treated: `'auto'` (default — post as-is, snapshot-and-retry on `DataCloneError`, warn once), `'strict'` (throw on `DataCloneError`, never recover), or `'snapshot'` (eagerly snapshot every envelope). - **`clearCloneWarnings()`** — reset the once-per-type auto-recovery warnings. ```ts import { setCloneMode, toCloneableSnapshot } from '@napplet/core'; // Default 'auto' just works — reactive state is snapshotted on the failure path. napplet.outbox.subscribe(filters, { relays, timeoutMs: 3000 }); // Or normalize explicitly / eagerly: napplet.outbox.subscribe(toCloneableSnapshot(filters), { relays }); setCloneMode('snapshot'); ``` These helpers are SDK plumbing only — the bytes placed on the wire are identical plain envelopes, so they add no protocol surface. ### Protocol types & constants - **`NostrEvent`**, **`NostrFilter`**, **`EventTemplate`**, **`Subscription`** — shared Nostr structures used by NAP domains such as outbox, relay, and identity. - **`Capability`** / **`ALL_CAPABILITIES`** — the human-readable capability strings (`relay:read`, `relay:write`, `sign:event`, `sign:nip44`, `state:read`, …). - **`PROTOCOL_VERSION`** (`'4.0.0'`), **`SHELL_BRIDGE_URI`** (`'napplet://shell'`), **`REPLAY_WINDOW_SECONDS`** (`30`), and the legacy **`TOPICS`** routing constants. ### Convention boundaries The stable identity is the complete, queryless `napplet:<archetype>/<intent>` string. Archetype manifest tags advertise accepted queryless convention identities. Routing and handler resolution use exact equality. INC `emit(topic, payload?)` accepts a convention URI. Unique percent-decoded query pairs become a shallow string-to-string payload before `postMessage`; literal `+` remains `+`. The normalized topic is queryless: ```ts napplet.inc.emit('napplet:profile/open?pubkey=abc123'); napplet.inc.on('napplet:profile/open', (event) => validateLocally(event.payload)); const result = await napplet.intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); if (!result.ok) throw new Error(result.error); ``` Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). This package defers to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) documents. ## Usage Register a handler and dispatch a message through an isolated registry: ```ts import { createDispatch } from '@napplet/core'; const { registerNap, dispatch } = createDispatch(); registerNap('outbox', (msg) => { // handles all outbox.* messages console.log('outbox message:', msg.type); }); dispatch({ type: 'outbox.query', id: 'abc', filters: [{ kinds: [1] }] }); // true dispatch({ type: 'unknown.action' }); // false dispatch({ type: 'malformed' }); // false (no dot) ``` ## See also - [Core concepts](https://napplet.run/docs/guide/concepts.md) — the JSON envelope and NAP dispatch model - [NIP-5D explained](https://napplet.run/docs/guide/nip-5d.md) — the wire format this package implements ### Optional shell environment The runtime may expose `shell` for environment information. `shell.supports(domain)` reads current domain object presence, `shell.services` is empty until environment delivery, and `shell.ready()` / `shell.onReady(handler)` use the retained snapshot. Readiness does not gate other domain calls. See the living [NAP-SHELL](https://github.com/napplet/naps/blob/master/naps/NAP-SHELL.md). ### Intent delivery and recommendations ```ts import { intent } from '@napplet/sdk'; intent.onDelivery(({ sender, convention, payload }) => { console.log(sender, convention, payload); }); await intent.invoke('napplet:profile/open?pubkey=abc123'); ``` The runtime binding retains deliveries until a listener registers. Discovery candidates expose opaque catalog `id` values and `contracts` with parameter names. INC peers instead use identifiers for running authenticated endpoints. A NAP-INTENT URI may append a bare `#naddr1…` recommendation; the binding sends its coordinate and relay hints as `handlerHint`, outside convention identity and payload. An applicable user default takes precedence. INC URI operations reject fragments. See the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). ## LLM types `LlmApi` and the LLM records describe `NappletGlobal.llm`, an optional domain for user-selected, runtime-mediated language model inference: connection selection and revalidation, stateless generation with tool definitions, content parts, and optional block-lifecycle deltas, and cancellation. `LlmErrorCode` lists the specification categories while staying open, because unknown codes remain failures. See the non-normative [LLM guide](https://napplet.run/docs/naps/llm.md) and living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134). --- <!-- https://napplet.run/docs/packages/index.md --> # Packages The napplet SDK is a small set of focused, ESM-only packages. Runtimes use **`@napplet/shim`** to inject selected domains. Napplets use **`@napplet/sdk`** and/or `@napplet/core` types, with **`@napplet/vite-plugin`** as a dev dependency for manifest generation. | Package | Description | | --- | --- | | [`@napplet/core`](https://napplet.run/docs/packages/core.md) | JSON envelope types (`NappletMessage`, `NapDomain`), NAP dispatch infrastructure (`registerNap`, `dispatch`), protocol constants and Nostr types. Imported by all other packages. | | [`@napplet/shim`](https://napplet.run/docs/packages/shim.md) | Runtime-side helper for injecting selected `window.napplet.<domain>` objects before napplet scripts run. | | [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) | Named TypeScript exports wrapping `window.napplet` for bundler consumers — `relay`, `inc`, `storage`, `keys`, and more, plus type re-exports. | | [`@napplet/nap`](https://napplet.run/docs/packages/nap.md) | All active domain subpaths as layered exports (barrel / types / shim / sdk per active NAP domain, plus ifc compatibility). Tree-shakable. | | [`@napplet/vite-plugin`](https://napplet.run/docs/packages/vite-plugin.md) | Vite plugin for napplet manifest generation: the final artifact hash, a NIP-5D kind 35129 event, description content, and capability/intent metadata at build time. | | [`@napplet/cli`](https://napplet.run/docs/packages/cli.md) | Standalone CLI for creating projects, owning deploy metadata, previewing in Paja, and deploying to Blossom servers and Nostr relays. | | [`@napplet/conformance`](https://napplet.run/docs/packages/conformance.md) | Framework-agnostic protocol conformance engine — reference mock shell, per-NAP envelope validators, manifest validator, and reporters. A dev/test tool, not loaded in the sandbox. | | [`@napplet/conformance-cli`](https://napplet.run/docs/packages/conformance-cli.md) | The headless `napplet-conformance` runner — drives the conformance engine against a napplet in real Chromium via Playwright. Wire it up as `test:conformance`. | | [`@napplet/conformance-web`](https://napplet.run/docs/packages/conformance-web.md) | Browser conformance runtime deployed at `/conformance` and bundled by the CLI UI/watch mode. | | [`@napplet/boilerplate`](https://napplet.run/docs/packages/boilerplate.md) | Interactive `npx` generator that clones the `github.com/napplet/boilerplate` template into a Vite + TypeScript starter. | ## Dependency graph ``` @napplet/shim ──► @napplet/nap ──► @napplet/core @napplet/sdk ──► @napplet/core @napplet/vite-plugin (build-time only, depends on nostr-tools) @napplet/cli (Deno deploy and diagnostics tool) @napplet/conformance-cli ──► @napplet/conformance + @napplet/conformance-web @napplet/conformance-web ──► @napplet/conformance @napplet/boilerplate (CLI generator, clones github.com/napplet/boilerplate) ``` The iframe sandbox requires only `allow-scripts` — **no `allow-same-origin`**. Napplets cannot access the host shell's DOM, cookies, `localStorage`, or service workers; all persistent state goes through the shell's proxies. > Every package is published to [npm](https://www.npmjs.com/org/napplet); most are > also on [JSR](https://jsr.io/@napplet) (`@napplet/boilerplate`, > `@napplet/conformance-cli`, and `@napplet/conformance-web` are npm-only). > Source lives at [github.com/napplet/web](https://github.com/napplet/web); > discussion happens in the > [community group chat](https://armada.buzz/invite/naddr1qvzqqqyzz5pzpjk98hj7z978r9xc9d2ymagw6tga0lx0s06y8lhpy9twc2kp8uwdqqqqpwqpw5#BAACAwTDEKKhS9_iA_qOc1n4ljVt). --- <!-- https://napplet.run/docs/packages/nap.md --> # @napplet/nap > Every active napplet NAP domain as layered subpath exports. The package name remains > `@napplet/nap` for compatibility. `@napplet/nap` ships every active NAP domain (relay, storage, inc, keys, theme, media, notify, identity, config, resource, cvm, outbox, upload, intent, ble, webrtc, link, count, lists, common, serial, fs, dm, llm) as independent, tree-shakable subpaths. It sits between the shim/sdk and [`@napplet/core`](https://napplet.run/docs/packages/core.md) in the dependency graph. - **npm:** [`@napplet/nap`](https://www.npmjs.com/package/@napplet/nap) - **JSR:** [`@napplet/nap`](https://jsr.io/@napplet/nap) - **Source:** [packages/nap](https://github.com/napplet/web/tree/main/packages/nap) NAP proposal status and shipped implementation maturity are separate. All capabilities can change. Inspect `NAP_IMPLEMENTATIONS` from `@napplet/core` and use `selectNapDomains` for local host opt-in policy. See [maturity and opt-in guidance](https://napplet.run/docs/naps/maturity.html). ## Install ```bash pnpm add @napplet/nap ``` > **No root export** > > `@napplet/nap` has **no root export** — you must import from a domain subpath. > `import '@napplet/nap'` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` by design. ## Subpath patterns Each domain exposes up to four entry-point shapes. Pick the one that matches what your code actually needs: | Pattern | Subpath | Contents | | --- | --- | --- | | **Barrel** | `@napplet/nap/<domain>` | types + shim installer + SDK helpers | | **Types-only** | `@napplet/nap/<domain>/types` | pure TypeScript types, zero runtime | | **Shim** | `@napplet/nap/<domain>/shim` | installer + message handlers (for shells) | | **SDK** | `@napplet/nap/<domain>/sdk` | named helper functions (for napplet code) | ```ts // Barrel — everything for a domain import { installRelayShim, relaySubscribe, RelaySubscribeMessage } from '@napplet/nap/relay'; // Types only — zero runtime cost import type { IncEventMessage } from '@napplet/nap/inc/types'; // Shim only — for shells mounting a NAP into the napplet window import { installStorageShim } from '@napplet/nap/storage/shim'; // SDK only — typed wrapper for napplet consumer code import { notifySend } from '@napplet/nap/notify/sdk'; ``` ## Tree-shaking contract - Published with `sideEffects: false`. - The `exports` map declares **96 entry points**: 23 active domain barrels, 23 active-domain types entries, 23 shim entries, 23 sdk entries, plus the `ifc` compatibility wrapper. - A bundler importing only `@napplet/nap/relay/types` produces zero bytes from the other domains. ## Domain notes - **resource** — a single scheme-pluggable byte-fetching primitive, `bytes(url) → Blob`, over four canonical schemes (`data:`, `https:`, `blossom:<sha256>.<ext>` per [BUD-10](https://github.com/hzrd149/blossom/blob/master/buds/10.md), `nostr:<bech32>`). - **identity** — strictly **read-only**: it exposes the shell-user pubkey and public identity data but never signs, encrypts, or decrypts. Take one snapshot with `getPublicKey()`, then subscribe to shell-pushed `identity.changed`. - **media** — ownership-aware media sessions with optional context links for queue position and related Nostr resources; the shell owns playback policy for shell-owned sessions. - **ble** — runtime-mediated Bluetooth LE/GATT sessions. Napplets use shell-scoped sessions and byte arrays while the shell owns chooser UI, permissions, device handles, GATT lifecycle, notifications, and policy. - **webrtc** — runtime-mediated WebRTC data sessions. Napplets use shell-scoped sessions while the shell owns signaling, SDP, ICE, and peer-connection lifecycle. - **link** — shell-mediated external navigation via `open(url, options?)`. The shell owns prompting, policy, opener isolation, and browser context. - **count** — runtime-mediated event counts via `query(filters, options?)`. The runtime owns relay COUNT support, indexing, aggregation, approximation, and refusal policy. - **lists** — runtime-mediated NIP-51 list mutations via `supported`/`add`/`remove`; the runtime owns lookup, merge, encryption, signing, and publishing. - **common** — shell-mediated public NIP-19 helpers, profile lookup returning `RelayEventResult`, follows, follow/unfollow, reactions, and reports; the shell owns identity, consent, event construction, signing, publishing, relay access, and NIP-19 handling. - **serial** — runtime-mediated serial device access: napplets get `open`/`write`/`close`/`onEvent`; the shell owns permissions, raw port handles, streams, OS paths, and lifecycle policy. - **fs** — shell-mediated virtual filesystem access: napplets get `info`/`pickFile`/`pickFiles`/`pickDirectory`/`pickSaveFile`/`stat`/`list`/`read`/`write`/`mkdir`/`remove`/`move`/`watch`/`unwatch`/`onChanged`; the runtime owns host paths, mounts, backing store, normalization, policy, and authorization. Byte payloads use RFC 4648 standard padded base64 text on the JSON wire, and byte limits/counts refer to decoded bytes. - **intent** — archetype-based invocation through `invoke(uri, options?)` and `open(uri, options?)`, with canonical structured dispatch results. ### INC convention URIs NAP-INC exposes `emit(topic, payload?)` and `on(topic, callback)`. For `emit('napplet:profile/open?pubkey=abc123')`, the binding transposes the query at the outgoing boundary into the shallow decoded text payload `{ pubkey: 'abc123' }` and posts the stable topic `napplet:profile/open`. Subscribe with the stable, queryless topic and keep routing exact afterward: ```ts import { emit, on } from '@napplet/nap/inc/sdk'; emit('napplet:profile/open?pubkey=abc123'); const sub = on('napplet:profile/open', (payload) => { // `pubkey` is a local convention choice; validate received payloads here. console.log(payload); }); ``` Fragments, malformed percent escapes, repeated decoded names, and a query with an explicit payload reject before emission. Structured or non-text data belongs in the explicit payload of a queryless topic. ### Intent invocation NAP-INTENT accepts `invoke(uri, options?)` and `open(uri, options?)`: ```ts import { intentAvailable, intentOpen, } from '@napplet/nap/intent/sdk'; if ((await intentAvailable('profile')).available) { const result = await intentOpen('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); if (!result.ok) console.error(result.error); } ``` Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). These APIs defer to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) documents. See the [NAP domain reference](https://napplet.run/docs/naps/index.md) for the full list with one-line purposes. ## Optional peer dependency `@napplet/nap` declares `json-schema-to-ts` as an **optional** peer dependency (scoped to the `config` domain's `FromSchema` typing). Install it only if you want schema-inferred typing for your `config.subscribe` callback; skipping it costs nothing. ## See also - [NAP domain reference](https://napplet.run/docs/naps/index.md) — every domain and its purpose - [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) — re-exports the per-domain helpers and message types ### Optional shell environment The runtime may expose `shell` for environment information. `shell.supports(domain)` reads current domain object presence, `shell.services` is empty until environment delivery, and `shell.ready()` / `shell.onReady(handler)` use the retained snapshot. Readiness does not gate other domain calls. See the living [NAP-SHELL](https://github.com/napplet/naps/blob/master/naps/NAP-SHELL.md). ### Intent delivery and recommendations ```ts import { intent } from '@napplet/sdk'; intent.onDelivery(({ sender, convention, payload }) => { console.log(sender, convention, payload); }); await intent.invoke('napplet:profile/open?pubkey=abc123'); ``` The runtime binding retains deliveries until a listener registers. Discovery candidates expose opaque catalog `id` values and `contracts` with parameter names. INC peers instead use identifiers for running authenticated endpoints. A NAP-INTENT URI may append a bare `#naddr1…` recommendation; the binding sends its coordinate and relay hints as `handlerHint`, outside convention identity and payload. An applicable user default takes precedence. INC URI operations reject fragments. See the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). ### Controller snapshots This non-normative guidance defers to [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108). When the runtime exposes `window.napplet.gamepad`, the SDK provides cached `getGamepads()`, read-only `available` and `focused`, and `onChange(handler)` with an independently closable listener. Keep keyboard or pointer controls available when the domain or native controllers are unavailable. Ignore gameplay edges while unfocused, and reset the button baseline on regained focus. Preserve null slots and route controllers by their `index`. ```ts import { gamepad } from '@napplet/sdk/gamepad'; if (window.napplet?.gamepad) { const subscription = gamepad.onChange(state => { if (!state.available || !state.focused) return; for (const pad of state.pads) { if (pad) console.log(pad.index, pad.axes); } }); // Call subscription.close() when the owning view is disposed, not right away. } ``` Declare `gamepad` as optional (`O`) when other controls fulfill the core task. Use a hard requirement (`R`) only when the core task needs controller input. The runtime owns controller access and focus isolation; app code does not call native `navigator.getGamepads()` or open raw HID devices. Haptics and remapping are outside this proposal. ### Language model inference `@napplet/nap/llm` provides the barrel, `types`, `shim`, and `sdk` subpaths for the four NAP-LLM operations. SDK helpers use the `llm` prefix (`llmSelect`, `llmDescribe`, `llmGenerate`, `llmCancel`), `isLlmFailure` recognizes rejections that carry a specification error code, and `LLM_ERROR_CODES` lists the known codes; `llmText` concatenates a generation's text parts and `llmToolCalls` returns tool calls only from a `toolUse` result. `generate` exposes the generation's `requestId`, streams ordered block-lifecycle deltas to an `onDelta` listener, and maps an abort signal to `llm.cancel`. Selection does not authorize disclosure; the runtime consents each generation. This non-normative guidance defers to the living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134). --- <!-- https://napplet.run/docs/packages/sdk.md --> # @napplet/sdk > Named TypeScript exports for napplet developers using a bundler. Wraps > `window.napplet` at call time. `@napplet/sdk` gives napplet code typed, named imports that delegate to injected `window.napplet.*` counterparts. It depends on [`@napplet/core`](https://napplet.run/docs/packages/core.md) for types only and has **no side effects**. If a method is called before the runtime injected `window.napplet`, or before that domain is available, the SDK throws a clear error. - **npm:** [`@napplet/sdk`](https://www.npmjs.com/package/@napplet/sdk) - **JSR:** [`@napplet/sdk`](https://jsr.io/@napplet/sdk) - **Source:** [packages/sdk](https://github.com/napplet/web/tree/main/packages/sdk) NAP proposal status and shipped implementation maturity are separate. All capabilities can change. Inspect `NAP_IMPLEMENTATIONS` from `@napplet/core` and use `selectNapDomains` for local host opt-in policy. See [maturity and opt-in guidance](https://napplet.run/docs/naps/maturity.html). ## Install ```bash npm install @napplet/sdk ``` ## Key exports Top-level namespaced objects that mirror `window.napplet`: - **`outbox`** — `getEvent`, `query`, `subscribe`, `publish`, `resolveRelays` - **`common`** — profile lookup, follow/unfollow, reactions, reports, NIP-19 helpers - **`lists`** — NIP-51 list read and mutation helpers - **`count`** — count queries through the shell - **`dm`** — shell-mediated encrypted direct-message helpers - **`relay`** — low-level explicit relay proxy; use only for relay-local escape hatches - **`inc`** — `emit`, `on` (plus deprecated `ifc*` migration aliases) - **`intent`** — `invoke`, `open`, `available`, `handlers`, `onChanged` - **`storage`** — `getItem`, `setItem`, `removeItem`, `keys`, plus `storage.instance.*` (per-instance scope) - **`keys`** — `registerAction`, `unregisterAction`, `onAction` - **`media`** — `createSession`, `reportState`, `onCommand`, … - **`notify`** — `send`, `badge`, `onAction`, … - **`config`** — `get`, `subscribe`, `openSettings`, `registerSchema`, `schema` - **`llm`** — User-selected, runtime-mediated language model inference: `select`, `describe`, `generate`, `cancel` - **`resource`** — `info`, `bytes`, `bytesMany`, `bytesAsObjectURL` `identity` is exported as a top-level object and through bare-name helpers: - `identityGetPublicKey`, `identityOnChanged` There is no top-level `shell` object. Detect capability availability from runtime-injected domain presence (`window.napplet?.outbox`, `window.napplet?.dm`, and so on). The SDK also re-exports: - the `*_DOMAIN` constants and `install*Shim` installers - `resourceInfo`, `resourceBytes`, `resourceBytesMany`, `resourceBytesAsObjectURL` It also re-exports the protocol types from `@napplet/core` and the per-domain message-type unions (`RelayNapMessage`, `IdentityNapMessage`, …) and `*_DOMAIN` constants from the NAP packages. ## Usage ```ts import { outbox, common, inc, intent, storage, keys, config, resource } from '@napplet/sdk'; // Read kind 1 notes through outbox-aware routing const { events } = await outbox.query( [{ kinds: [1], limit: 20 }], { timeoutMs: 3000 }, ); for (const result of events) console.log('Note:', result.event.content); // Subscribe to live updates through the same outbox boundary const sub = outbox.subscribe([{ kinds: [1], limit: 20 }], { timeoutMs: 3000 }); sub.on('event', (result) => console.log('New note:', result.event.content)); // Publish a signed note through the user's outbox/write relays const published = await outbox.publish({ kind: 1, content: 'Hello from my napplet!', tags: [], created_at: Math.floor(Date.now() / 1000), }); if (!published.ok || !published.event) throw new Error(published.error ?? 'publish failed'); // Common social actions keep consent, event construction, signing, and relay routing in the shell await common.react(published.event.id, '+'); // Inter-napplet messaging: payload is a local convention choice. inc.emit('chat:message', { text: 'hi' }); const intentResult = await intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); if (!intentResult.ok) throw new Error(intentResult.error); // Scoped storage await storage.setItem('theme', 'dark'); // Live per-napplet config const configSub = config.subscribe((values) => applyTheme(values.theme)); // Fetch external bytes through the shell (`blossom:<sha256>.<ext>` URIs follow BUD-10: https://github.com/hzrd149/blossom/blob/master/buds/10.md) const avatarBlob = await resource.bytes('blossom:<sha256>.png', { servers: ['https://cdn.hzrd149.com'], }); const avatarItems = await resource.bytesMany([ { url: 'https://example.com/avatar.png' }, { url: 'blossom:<sha256>.png', servers: ['https://cdn.hzrd149.com'] }, ]); ``` ### INC convention URIs `inc.emit(topic, payload?)` accepts a queried convention URI at the INC developer boundary. The binding transposes a call such as `inc.emit('napplet:profile/open?pubkey=abc123')` into the queryless stable topic `napplet:profile/open` plus a shallow decoded text payload. `pubkey` is a local convention choice; receiving code owns payload validation. Subscribe using the stable topic, not the queried developer-facing URI: ```ts inc.emit('napplet:profile/open?pubkey=abc123'); const profileOpen = inc.on('napplet:profile/open', (event) => { validateProfileOpenPayload(event.payload); }); ``` Routing remains exact after transposition, with no query-aware, prefix, or wildcard matching. Fragments, malformed percent encoding, repeated decoded names, and a query combined with an explicit payload reject before emission. Manifest convention values and normalized wire identities stay queryless. ### Intent dispatch Use `intent.invoke(uri, options?)` or `intent.open(uri, options?)`. ```ts const result = await intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); if (!result.ok) throw new Error(result.error); ``` Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). These APIs defer to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) documents. ### Typed config with `FromSchema` `json-schema-to-ts` is an **optional** peer dependency. Install it to get `FromSchema<typeof schema>` typing flowing into your `config.subscribe` callback; skip it and `config.subscribe` still works with the default `Record<string, unknown>` typing. ```ts import { config } from '@napplet/sdk'; import type { FromSchema } from 'json-schema-to-ts'; const schema = { type: 'object', properties: { theme: { type: 'string', enum: ['light', 'dark'], default: 'dark' } }, required: ['theme'], } as const; const sub = config.subscribe((values: FromSchema<typeof schema>) => { // values.theme is typed 'light' | 'dark' }); ``` ## Namespace import `import * as napplet from '@napplet/sdk'` produces an object structurally identical to `window.napplet`: ```ts import * as napplet from '@napplet/sdk'; const { events } = await napplet.outbox.query([{ kinds: [1], limit: 20 }]); ``` ## See also - [Runtime injection vs. SDK](https://napplet.run/docs/guide/getting-started.md#runtime-injection-vs-sdk) - [`@napplet/shim`](https://napplet.run/docs/packages/shim.md) — runtime-side injected global installer ### Optional shell environment The runtime may expose `shell` for environment information. `shell.supports(domain)` reads current domain object presence, `shell.services` is empty until environment delivery, and `shell.ready()` / `shell.onReady(handler)` use the retained snapshot. Readiness does not gate other domain calls. See the living [NAP-SHELL](https://github.com/napplet/naps/blob/master/naps/NAP-SHELL.md). ### Intent delivery and recommendations ```ts import { intent } from '@napplet/sdk'; intent.onDelivery(({ sender, convention, payload }) => { console.log(sender, convention, payload); }); await intent.invoke('napplet:profile/open?pubkey=abc123'); ``` The runtime binding retains deliveries until a listener registers. Discovery candidates expose opaque catalog `id` values and `contracts` with parameter names. INC peers instead use identifiers for running authenticated endpoints. A NAP-INTENT URI may append a bare `#naddr1…` recommendation; the binding sends its coordinate and relay hints as `handlerHint`, outside convention identity and payload. An applicable user default takes precedence. INC URI operations reject fragments. See the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). Theme updates reach napplets with the exposed `theme` domain, independently of manifest requirement declarations. User identity changes report the connected signer, including an empty string at sign-out; they do not alter napplet identity. Profile picture/banner bytes use optional `resource` when exposed; otherwise show local placeholder artwork. Theme media URLs do not grant network access, so colors and local fonts provide the fallback. See [NAP-THEME](https://github.com/napplet/naps/blob/master/naps/NAP-THEME.md) and [NAP-IDENTITY](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md). ### Controller snapshots This non-normative guidance defers to [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108). When the runtime exposes `window.napplet.gamepad`, the SDK provides cached `getGamepads()`, read-only `available` and `focused`, and `onChange(handler)` with an independently closable listener. Keep keyboard or pointer controls available when the domain or native controllers are unavailable. Ignore gameplay edges while unfocused, and reset the button baseline on regained focus. Preserve null slots and route controllers by their `index`. ```ts import { gamepad } from '@napplet/sdk/gamepad'; if (window.napplet?.gamepad) { const subscription = gamepad.onChange(state => { if (!state.available || !state.focused) return; for (const pad of state.pads) { if (pad) console.log(pad.index, pad.axes); } }); // Call subscription.close() when the owning view is disposed, not right away. } ``` Declare `gamepad` as optional (`O`) when other controls fulfill the core task. Use a hard requirement (`R`) only when the core task needs controller input. The runtime owns controller access and focus isolation; app code does not call native `navigator.getGamepads()` or open raw HID devices. Haptics and remapping are outside this proposal. ### Language model inference This non-normative guide follows the living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134). Import `llm` from `@napplet/sdk` or `@napplet/sdk/llm`; root exports include prefixed helpers such as `llmSelect` and `llmGenerate`. The dedicated subpath also provides bare aliases such as `select` and `generate`. The API exposes `select`, `describe`, `generate`, and `cancel`. `generate` returns a pending promise that exposes `requestId`; pass `onDelta` for ordered provisional `start` / `delta` / `end` block events and `signal` to map an abort to `llm.cancel`, and replace provisional output with the terminal `content` parts. `llmText(generation)` concatenates the text parts; `llmToolCalls(generation)` returns tool calls only from a `finish: "toolUse"` result. Each method resolves with the proposal's result record; a runtime `Error` record rejects with an `Error` carrying the specification `code`, recognizable with `isLlmFailure(error)`, while the binding's own transport timeout rejects without a `code`. ```ts import { llm, isLlmFailure, llmText } from '@napplet/sdk/llm'; if (window.napplet?.llm) { try { const model = await llm.select({ json: true }); const generation = await llm.generate({ handle: model.handle, messages: [{ role: 'user', content: [{ type: 'text', text: 'Return a JSON object with a greeting.' }] }], format: 'json', }); if (generation.finish === 'stop') render(JSON.parse(llmText(generation))); } catch (error) { if (isLlmFailure(error)) console.warn(error.code, error.message); else throw error; } } ``` Selection does not authorize disclosure: the runtime obtains consent before each generation and may answer `user-cancelled`. Only `finish: "stop"` is complete output. Tool calls are requests the napplet validates and executes itself, if at all, through separately authorized capabilities; the runtime never executes them, and tool results re-enter the conversation as untrusted input. Handles are endpoint-scoped and expire when the connection, account, or endpoint changes. The runtime owns connections, credentials, consent, and provider access; model output is untrusted data. --- <!-- https://napplet.run/docs/packages/shim.md --> # @napplet/shim > Runtime-side helper for injecting selected `window.napplet.<domain>` objects > before napplet scripts run. `@napplet/shim` is consumed by NIP-5D runtimes, not by napplet application code. The runtime calls its installer before any napplet script runs, injecting only the NAP domain objects exposed to that napplet. It has no cryptographic dependencies — the shim sends messages, and the shell handles identity, signing, and encryption. **No `window.nostr` is installed.** - **npm:** [`@napplet/shim`](https://www.npmjs.com/package/@napplet/shim) - **JSR:** [`@napplet/shim`](https://jsr.io/@napplet/shim) - **Source:** [packages/shim](https://github.com/napplet/web/tree/main/packages/shim) ## Maturity-controlled injection Use the side-effect-free `@napplet/shim/prelude` entrypoint for policy-controlled hosts. The legacy root `@napplet/shim` import still installs every bundled domain for compatibility. ```ts import { selectNapDomains } from '@napplet/core'; import { renderNappletRuntimePreludeCall } from '@napplet/shim/prelude'; const selection = selectNapDomains({ candidates: ['storage', 'llm', 'ble'], optIn: ['llm'], }); const activate = renderNappletRuntimePreludeCall({ domains: selection.domains }); ``` Inline the maintained prelude artifact followed by `activate` before napplet scripts run. Enforce the same `selection.domains` set in host dispatch: hiding a shim object does not block direct `postMessage`. Candidates must already be implemented and authorized for the napplet. Prelude helpers also accept `maturity: {}` for supported-only filtering or `maturity: { optIn: ['llm'] }` for a named exception. Omitting `maturity` preserves existing explicit allowlist behavior. An explicit install replaces an earlier legacy namespace but cannot undo earlier calls; finish host setup before napplet code executes. See the [maturity guide](https://napplet.run/docs/naps/maturity.html) for classifications, required/optional domain handling, and provenance limits. ## Install ```bash npm install @napplet/shim ``` ## Runtime export `installNappletGlobal` installs selected domain objects onto a target window. Napplet-side code should use [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) or direct typed `window.napplet` access. For `iframe.srcdoc` runtimes, `@napplet/shim/prelude` exposes a host-injectable surface that does not require every napplet bundle to import the shim. Inline the npm browser artifact from `@napplet/shim/prelude.global`, then activate it with an explicit domain allowlist: ```ts import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { renderNappletRuntimePreludeCall } from '@napplet/shim/prelude'; const require = createRequire(import.meta.url); const preludeSource = readFileSync( require.resolve('@napplet/shim/prelude.global'), 'utf8', ); const activatePrelude = renderNappletRuntimePreludeCall({ domains: ['identity', 'storage', 'outbox'], }); const srcdoc = html.replace( '<head>', `<head><script>${preludeSource}\n${activatePrelude}</script>`, ); ``` The IIFE artifact exposes `globalThis.NappletShimPrelude.install({ domains })` and installs only the requested known NAP domains. JSR exposes the source ESM helpers under `@napplet/shim/prelude`; the generated `prelude.global` artifact is npm-only. ## The `window.napplet` shape After runtime injection, the global may be populated with these sub-objects: | Namespace | What it does | | --- | --- | | `outbox` | Outbox-aware `getEvent`, `query`, `subscribe`, `publish`, and `resolveRelays`; default for normal event reads and publishes | | `common` | Profile lookup, follow/unfollow, reactions, reports, and NIP-19 helpers | | `lists` | NIP-51 list read and mutation helpers | | `count` | Count queries through the shell | | `dm` | Shell-mediated encrypted direct-message helpers | | `relay` | Low-level explicit relay proxy for relay-local escape hatches | | `inc` | Inter-napplet communication: `emit`, `on` | | `intent` | Archetype-based intent invocation, discovery, and availability changes | | `storage` | Scoped key-value storage: `getItem`, `setItem`, `removeItem`, `keys` (512 KB quota), plus `storage.instance.*` for per-instance scope | | `keys` | Keyboard forwarding + action keybindings: `registerAction`, `unregisterAction`, `onAction` | | `media` | Ownership-aware media sessions: `createSession`, `reportState`, `onCommand`, … | | `notify` | Shell-rendered notifications: `send`, `badge`, `onAction`, … | | `identity` | Read-only user queries: `getPublicKey`, `onChanged`, `getProfile`, … | | `config` | Per-napplet declarative config: `get`, `subscribe`, `openSettings`, `registerSchema`, `schema` | | `resource` | Sandboxed byte fetching: `info`, `bytes`, `bytesMany`, `bytesAsObjectURL` | | Domain absence | If a property is absent, that NAP is unavailable to the napplet. | ## Usage ```ts import { installNappletGlobal } from '@napplet/shim'; installNappletGlobal({ domains: ['outbox', 'storage', 'identity', 'inc', 'intent'], }); ``` Napplet application code then consumes injected domains: ```ts // Read kind 1 notes through outbox-aware routing const { events } = await window.napplet.outbox.query( [{ kinds: [1], limit: 20 }], { timeoutMs: 3000 }, ); for (const result of events) console.log('Note:', result.event.content); // Subscribe to live updates through the same outbox boundary const sub = window.napplet.outbox.subscribe([{ kinds: [1], limit: 20 }], { timeoutMs: 3000, }); sub.on('event', (result) => console.log('New note:', result.event.content)); // Publish a note (the shell signs and fans it out) const published = await window.napplet.outbox.publish({ kind: 1, content: 'Hello from my napplet!', tags: [], created_at: Math.floor(Date.now() / 1000), }); if (!published.ok) throw new Error(published.error ?? 'publish failed'); // NAP-INC convention URI emission: the runtime sends the stable topic with // a shallow text payload. `pubkey` is a local convention choice. window.napplet.inc.emit('napplet:profile/open?pubkey=abc123'); const profileOpen = window.napplet.inc.on('napplet:profile/open', (event) => { validateProfileOpenPayload(event.payload); }); const intentResult = await window.napplet.intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); // Scoped storage, proxied through the shell await window.napplet.storage.setItem('theme', 'dark'); const theme = await window.napplet.storage.getItem('theme'); // 'dark' // Read-only identity const pubkey = await window.napplet.identity.getPublicKey(); // "" when signed out // Feature-gate before using a domain if (window.napplet?.media) { const { sessionId } = await window.napplet.media.createSession({ owner: 'napplet', metadata: { title: 'My Song', artist: 'The Artist' }, }); } sub.close(); profileOpen.close(); ``` ### INC convention URIs The NAP-INC shim accepts a queried `napplet:<archetype>/<intent>` URI only at `emit(topic, payload?)`. It transposes the query before posting `inc.emit`, so the shell and consumers see the exact queryless stable topic and a shallow decoded text payload. Subscriptions and routing never parse, normalize, prefix-match, or wildcard-match topics after that boundary. Fragments, malformed percent escapes, repeated decoded names, and a queried URI with an explicit payload reject before emission. Use a queryless topic and its explicit payload for structured data. ### Intent dispatch injection Intent calls derive archetype and action from the URI. An `ok: true` result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an `ok: false` result includes `error`. Targets receive runtime-attested `IntentDelivery` values through `onDelivery`, including deliveries retained before registration. Behavior hints are `focus` and `reuse`. This non-normative guidance defers to the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md). This behavior defers to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) documents. ## TypeScript support The shim does not modify global `Window` types in its published source (so it is accepted by JSR). For typed `window.napplet` access, cast using `NappletGlobal` from [`@napplet/core`](https://napplet.run/docs/packages/core.md), or — preferably — use the named helpers in [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md): ```ts import type { NappletGlobal } from '@napplet/core'; const napplet = (window as Window & { napplet: NappletGlobal }).napplet; ``` ## Wire format The shim communicates with the shell using JSON envelope messages (`{ type: 'domain.action', ...payload }`). `installNappletGlobal` first opens the NIP-5D channel: it creates a `MessageChannel`, keeps one port private and hands the other to the shell once in `napplet.hello`. Outbound and inbound envelopes then travel only over that port; window-level messages are ignored. Request/response pairs are correlated by an `id` field. The shim package README documents the full per-domain outbound and inbound message catalog. ## See also - [Shim vs. SDK](https://napplet.run/docs/guide/getting-started.md#when-to-use-shim-vs-sdk) - [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) — named, typed wrapper over the same global ### Optional shell environment The runtime may expose `shell` for environment information. `shell.supports(domain)` reads current domain object presence, `shell.services` is empty until environment delivery, and `shell.ready()` / `shell.onReady(handler)` use the retained snapshot. Readiness does not gate other domain calls. See the living [NAP-SHELL](https://github.com/napplet/naps/blob/master/naps/NAP-SHELL.md). ### Intent delivery and recommendations ```ts import { intent } from '@napplet/sdk'; intent.onDelivery(({ sender, convention, payload }) => { console.log(sender, convention, payload); }); await intent.invoke('napplet:profile/open?pubkey=abc123'); ``` The runtime binding retains deliveries until a listener registers. Discovery candidates expose opaque catalog `id` values and `contracts` with parameter names. INC peers instead use identifiers for running authenticated endpoints. A NAP-INTENT URI may append a bare `#naddr1…` recommendation; the binding sends its coordinate and relay hints as `handlerHint`, outside convention identity and payload. An applicable user default takes precedence. INC URI operations reject fragments. See the living [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md). ### LLM runtime binding Select `llm` in `installNappletGlobal({ domains: ['llm'] })` or the equivalent runtime prelude options to expose `window.napplet.llm`. The binding forwards `select`, `describe`, `generate`, and `cancel`, correlates each result by `id` and type, routes `llm.delta` only to the generation that owns its `requestId`, drops deltas that do not match the Delta schema (`start` / `delta` / `end` events, `text` only on `delta`, `toolCallId` and `name` only on a `toolCall` start), stops provisional delivery on a sequence gap, rejects an `Error` record as an error carrying the specification code, and maps an abort signal to `llm.cancel`. Installing the binding supplies no connection, consent, or provider: the host provides policy-bound handlers, endpoint-scoped handles, and prompts. See the living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134). --- <!-- https://napplet.run/docs/packages/vite-plugin.md --> # @napplet/vite-plugin Vite build integration for current NIP-5D manifests. This package is a development dependency; shells provide the runtime namespace. This is non-normative package guidance. [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) defines event format and loading; [NAPs](https://github.com/napplet/naps) define capability contracts. ## Install and configure ```sh pnpm add -D @napplet/vite-plugin ``` ```ts import { defineConfig } from 'vite'; import { nip5aManifest } from '@napplet/vite-plugin'; export default defineConfig({ plugins: [nip5aManifest({ nappletType: 'notes', title: 'Notes', description: 'Read and write personal notes', requires: ['outbox', 'storage'], optional: ['theme'], archetypes: [{ slug: 'note', convention: 'napplet:note/open', params: ['id'] }], artifactMode: 'single-file', })], }); ``` `nip5aManifest` and `.nip5a-manifest.json` retain their existing names for package and tool compatibility. Their output is a NIP-5D manifest, not an nsite manifest. From **0.15.0**, the output uses the direct artifact hash; **0.14.x and earlier** generated legacy path/aggregate events. ## Build output The default `single-file` mode inlines local JS/CSS into `index.html`. After rewriting, the plugin hashes the final artifact bytes and writes a kind `35129` event template. Its single `x` tag is that SHA-256, and `content` is the description. No `path` tags or aggregate hash are emitted. Embedded metadata is part of those artifact bytes and therefore changes the hash when edited. The hash is never injected back into the bytes it covers. Set `description`, or supply an ordinary HTML `<meta name="description" content="…">`. A missing or empty description fails manifest generation because NIP-5D §Manifest requires non-empty content. `title` and `description` options update their HTML counterparts. The plugin also embeds the applicable publishing metadata described below before computing `x`. Without `VITE_DEV_PRIVKEY_HEX`, the sidecar is unsigned. With a development key, it contains a signed event. `artifactHash` is local sidecar metadata, not a Nostr tag. Use `napplet deploy` for production signing, Blossom upload and relay publication; it builds fresh events from the artifact and metadata. ## Options | Option | Behavior | | --- | --- | | `nappletType: string` | Named-event `d` identifier. | | `description?: string` | Plain-text event content; falls back to HTML description metadata. | | `title?: string` | Optional display title and HTML title override. | | `requires?: string[]` | Required domains, serialized as `R` tags. | | `requires?: { infer?, explicit?, mode? }` | Opt-in inference from static SDK/NAP imports and direct namespace access. `mode` is `warn` or `error` for missing explicit declarations. | | `optional?: string[]` | Optional integrations, serialized as `O` tags and excluded from inferred required domains. | | `archetypes?: Array<{ slug, convention, params? }>` | Existing ergonomic option; emits independent `z` role and `i` intent tags. `params` names advertised intent parameters. | | `intents?: Array<{ intent, params? }>` | Advertise intents without coupling them to a role option. | | `source?: string` | Source URL metadata. | | `servers?: string[]` | Blossom server hints. | | `icon?: { data?, sha256?, mimeType }` | Supply PNG, JPEG or WebP bytes as `Uint8Array` to embed an icon and derive its hash. Hash-only input remains supported; upload that blob separately. | | `screenshots?: Array<{ data?, sha256?, mimeType?, dim?, size?, alt? }>` | `screenshot` tags in display order. Bytes are hashed, sized, format-sniffed and measured; hash-only entries are emitted as given. See [Presentation media](#presentation-media-and-source-pins). | | `preview?: { data?, sha256?, mimeType?, dim?, size?, duration?, alt? }` | The single `preview` video tag (WebM or MP4 bytes, or a hash). | | `sourceCommit?: string` | Full lowercase-hex Git commit ID emitted as `source-commit`; clients ignore it without a repository `source`. | | `sourceArchive?: { url, sha256, mimeType }` | Independently downloadable source archive emitted as `source-archive` (HTTPS URL without credentials or fragment). | | `configSchema?: NappletConfigSchema \| string` | Inline NAP-CONFIG schema or project-relative schema file. Discovery falls back to `config.schema.json`, then `napplet.config.ts`/`.js`/`.mjs`. | | `artifactMode?: 'single-file' \| 'external-assets'` | `single-file` is the default. The explicit external-assets option preserves Vite output for existing tooling; external runtime assets need rebundling before deployment as a self-contained napplet. External-assets builds warn that the manifest `x` hash covers only `index.html` and list the remaining local assets. Builds without `dist/index.html` fail with a `[nip5a-manifest]` error. | Known-domain inference is best effort. Explicit required domains are retained with a build warning when absent from this package's registry; they still need a published NAP contract and support from the target shell. An `R` or `O` declaration never grants authority. Runtime code checks `window.napplet?.<domain>` and handles missing domains. Config schema discovery and validation remain package features. A resolved schema is carried in its NAP-defined `config` tag and is not folded into `x`. See the [NAP track](https://github.com/napplet/naps) for schema semantics. ```ts nip5aManifest({ nappletType: 'notes', description: 'Read and write notes', requires: { infer: true, explicit: ['outbox', 'storage'], mode: 'warn' }, optional: ['theme'], }); ``` ## Migration New builds emit only the current schema. For a legacy-only shell, use CLI 0.7.0+ with `napplet deploy --format legacy` during the compatibility window. That serializer is isolated in the CLI and can be removed without changing current readers. Existing immutable event pointers and shell ACL/storage keys need separate rollout planning; rebuilding does not migrate saved data. See the [event migration guide](https://napplet.run/docs/guide/event-migration.md). ## Recoverable HTML metadata This is a non-normative description of the plugin's implementation of [NIP-5D, HTML Metadata for Publishing](https://github.com/nostr-protocol/nips/pull/2303). The living specification defines the mappings and remains authoritative. Embedding supplies publishing hints; it is not a runtime requirement and does not grant capabilities or override a signed manifest. The final head contains the resolved title, description, named identifier, archetypes, accepted intents and advertised parameter names, required/optional domains, source URL, Blossom hints, and an icon data URL when image bytes are available. Screenshot, preview, `source-commit` and `source-archive` tags have no head mapping and stay in the sidecar. The plugin emits named events (`35129`), so it removes snapshot-only `napplet-parent` and `napplet-root` declarations. It never inserts the artifact hash or event signing fields into HTML. Explicit options replace the corresponding author head declarations; an omitted option falls back to the head. `nappletType` remains required and controls the identifier. Explicit `archetypes` replaces author archetype and intent declarations with its roles and conventions; `intents` adds standalone advertisements. Without `archetypes`, explicit `intents` replaces just the author intent declarations. Empty arrays clear a category. `requires: { infer: true, explicit: [...] }` combines the configured list with inferred domains; `optional` wins overlaps. The first author singleton is used, repeated declarations are deduplicated, and all owned head elements are replaced once. Scripts, comments, templates, body content, and unrelated head elements are preserved. Metadata inside scripts, comments, templates, or the body is not read as a declaration. For a recoverable icon, provide bytes: ```ts import { readFileSync } from 'node:fs' nip5aManifest({ nappletType: 'notes', description: 'Read and write notes', icon: { data: readFileSync(new URL('./icon.png', import.meta.url)), mimeType: 'image/png', }, }) ``` The hash covers decoded image bytes, not the data URL. If `sha256` is supplied alongside `data`, it is checked against those bytes. An existing supported head icon data URL can supply the bytes instead. Hash-only input retains its manifest tag; when no matching head image exists the plugin warns and omits the HTML icon, because a hash cannot recover an image. Unsupported, remote, empty, or malformed author icon URLs supply no embedded icon. Consumers still verify image decoding before display as specified by NIP-5D. Metadata and JS/CSS inlining finish before the plugin hashes `index.html`. Changing an embedded declaration or icon changes the artifact hash. The NAP-CONFIG schema remains in the sidecar: NIP-5D defines no HTML mapping for that extension. `napplet deploy` can recover the specified publishing fields and icon bytes from the built HTML without the sidecar. ## Presentation media and source pins [NIP-5D §Presentation media](https://github.com/nostr-protocol/nips/pull/2303) lets a manifest carry any number of `screenshot` tags and at most one `preview` video, each described by NIP-92 `key value` fields: one `x` sha256 of the complete bytes plus optional `m`, `dim`, `size`, `alt` and, for previews, `duration`. §Source adds an optional `source-commit` pin and an optional `source-archive`. The specification defines no HTML head mapping for these tags, so the plugin writes them only to the `.nip5a-manifest.json` template and never into `index.html`; adding media does not change the artifact hash. ```ts import { readFileSync } from 'node:fs' nip5aManifest({ nappletType: 'notes', description: 'Read and write notes', source: 'https://github.com/example/notes', sourceCommit: '0123456789abcdef0123456789abcdef01234567', screenshots: [ { data: readFileSync(new URL('./shots/board.png', import.meta.url)), alt: 'The notes board' }, { sha256: 'ae7a…c75a', mimeType: 'image/webp', dim: '1200x750' }, ], preview: { data: readFileSync(new URL('./shots/demo.webm', import.meta.url)), dim: '960x600', duration: 8.5 }, }) ``` With `data`, the plugin derives `x` and `size`, sniffs the format for `m` (PNG, JPEG or WebP for screenshots; WebM or MP4 for the preview), and reads `dim` from PNG, JPEG and WebP headers. A declared `sha256`, `mimeType`, `size` or `dim` must agree with the bytes or the build fails. Each supplied blob is written to `dist/<sha256>.<ext>` after `index.html` is hashed so deploy tooling can upload it to the declared Blossom servers; `napplet deploy` uploads blobs referenced by these tags. Hash-only entries keep their tag and need the blob uploaded separately. Because the specification says publishers SHOULD include `m` and `dim`, omitting them produces a build warning. `sourceCommit` without a `source` is still emitted but warns, since clients ignore a pin without a repository source; `sourceArchive` is validated for shape only. Clients verify hashes and decoding before display as specified by NIP-5D; declaring media does not grant anything. --- <!-- https://napplet.run/docs/spec.md --> # NIP-5D spec status The napplet packages implement **NIP-5D — *Nostr Web Applets***. This site does **not** host a copy of the specification — NIP-5D is a living document, and the canonical text is the only source of truth. ## Source of truth When the packages, this site, or any summary drift from these, **the living documents win**: - **NIP-5D** (the napplet ⇄ shell protocol): [github.com/nostr-protocol/nips/pull/2303](https://github.com/nostr-protocol/nips/pull/2303) - **NAPs track** (every NAP capability domain — `relay`, `storage`, `identity`, …): [github.com/napplet/naps](https://github.com/napplet/naps) The [NIP-5D explained](https://napplet.run/docs/guide/nip-5d.md) page on this site is a **non-normative** orientation only; for every normative requirement, read the canonical NIP-5D and the relevant NAP. ## Expect drift > **ALPHA — expect drift** > > napplet is **alpha**. The specification is experimental and a moving target. There > **will most certainly be drift** between the packages and the specification. > Things **will most certainly change**. Always treat the living documents above as > the source of truth, and pin to a specific package version when building. ## Related specs The NAP domains build on adjacent Nostr specs: - **NIP-5A** — source-tag semantics referenced by current NIP-5D. - **NIP-65** — relay-list metadata, used by the `outbox` domain. - **NIP-94 / NIP-96 + Blossom** — used by the `upload` domain. - **NIP-19** — bech32 entity encoding, used by the `resource` domain's `nostr:` scheme. See the [NAP domain reference](https://napplet.run/docs/naps/index.md) for how these map onto domains.