diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 6ecfc2a2..26d51fd6 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -929,3 +929,16 @@ Plans:
- [x] 162-01-PLAN.md — Current-schema writer and readers
- [x] 162-02-PLAN.md — CLI format boundary and event migration
- [x] 162-03-PLAN.md — Docs, release metadata and draft PR shipment
+
+### Phase 163: NAP-GAMEPAD across repositories
+
+**Goal:** Implement the proposed GAMEPAD API/runtime/tooling across all organization repositories and applicable packages, including boilerplate. Non-normative acceptance defers to [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108) and [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303).
+**Requirements:** GP-API, GP-WIRE, GP-ISOLATION, GP-TRUST, GP-REPOS, GP-DOCS, GP-SHIP (defined in 163-CONTEXT.md)
+**Depends on:** Phase 162
+**Plans:** 3 plans — COMPLETE
+
+- [x] 163-01-PLAN.md — Portable API, core types, runtime binding and SDK exports
+- [x] 163-02-PLAN.md — Host isolation, injection and conformance
+- [x] 163-03-PLAN.md — All repositories, docs, releases and coordinated PR shipment
+
+Verified and shipped in [web #242](https://github.com/napplet/web/pull/242), [Kehto #297](https://github.com/kehto/web/pull/297), and five consumer/community PRs linked in COVERAGE.md. Publish the Kehto package set before the primary release. The optional native API replacement conflict and existing CVM baseline failures remain disclosed.
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 783d0768..6e52a12d 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -2,17 +2,19 @@
gsd_state_version: 1.0
milestone: v0.34.0
milestone_name: NIP-5D Runtime Injection
-current_phase: 162
-current_phase_name: nip-5d-event-schema-migration
-status: complete
-stopped_at: "Phase 162 verified; draft PR #224 open with hzrd149 requested"
-last_updated: "2026-10-14T05:55:04.171122+00:00"
-last_activity: 2026-10-13
-last_activity_desc: "Completed quick task 261013-vo3: icon design and generation skill"
+current_phase: 163
+current_phase_name: nap-gamepad-across-repositories
+status: completed
+stopped_at: Phase 163 verified; completing prerequisite publication for CLI follow-up 243
+last_updated: "2026-10-14T16:24:49.491178+00:00"
+last_activity: 2026-10-14
+last_activity_desc: Integrated the merged icon skill with the verified GAMEPAD release
progress:
- total_plans: 3
- completed_plans: 3
- percent: 100
+ total_phases: 8
+ completed_phases: 3
+ total_plans: 16
+ completed_plans: 6
+ percent: 38
---
# Project State
@@ -23,18 +25,18 @@ See: .planning/PROJECT.md (updated 2026-05-24 after v0.31.0 archive)
**Core value:** Prove that sandboxed Nostr apps can securely delegate to a host shell over a simple, standardized protocol — and ship the spec + SDK so others can build on it.
-**Current focus:** Phase 162 — NIP-5D event schema migration; draft PR #224 awaiting review
+**Current focus:** Phase 163 — NAP-GAMEPAD verified and shipped as coordinated PRs; release ordering recorded in COVERAGE.md.
> **Provenance note:** The "Accumulated Context" section below preserves bullet records from BOTH branches' STATE.md histories. Records tagged "v0.29.0" from main's lineage refer to the milestone NOW renumbered as v0.30.0 (Class-Gated Decrypt — Phases 135-138). Records tagged "v0.29.0" from feat/strict-model refer to NUB-CONNECT (Phases 135-142). Phase number alone is not a unique identifier across the two; cross-reference the topic (decrypt/identity/NIP-07 → v0.30.0; connect/class/CSP-authority → v0.29.0).
## Current Position
-Phase: 162 (nip-5d-event-schema-migration) — COMPLETE
+Phase: 163 (nap-gamepad-across-repositories) — implementation verified
Plan: 3 of 3
-Status: Verified; [draft PR #224](https://github.com/napplet/web/pull/224) open, hzrd149 requested
-Last activity: 2026-10-13 - Completed quick task 261013-vo3: Add icon design and generation skill
+Status: Completing prerequisite publication for CLI follow-up #243
+Last activity: 2026-10-14 - Integrated the merged icon skill with the GAMEPAD release branch
-See phase 162 VERIFICATION.md for evidence and disclosed compatibility/tooling findings. Earlier phase and quick-task records below are historical.
+See phase 163 VERIFICATION.md and COVERAGE.md for evidence and disclosed release/specification/baseline limits. Earlier phase and quick-task records below are historical.
### Quick task 261011-hdg — COMPLETE
@@ -716,3 +718,7 @@ Resume: Phase 142 TERMINAL-COMPLETE — all 13 VER-IDs (VER-01..13) verified PAS
| Phase 161 P12 | 4min | 1 tasks | 5 files |
| Phase 161-ad-hoc-convention-package-contracts P15 | 2min | 1 tasks | 4 files |
| Phase 161 P16 | 4 min | 2 tasks | 5 files |
+
+### Phase 163 roadmap evolution
+
+- Added NAP-GAMEPAD across repositories, including boilerplate and relevant Kehto runtime consumers. Planning and implementation evidence live in phases/163-nap-gamepad-across-repositories.
diff --git a/.planning/debug/resolved/pr242-paja-release-order.md b/.planning/debug/resolved/pr242-paja-release-order.md
new file mode 100644
index 00000000..8eb8785b
--- /dev/null
+++ b/.planning/debug/resolved/pr242-paja-release-order.md
@@ -0,0 +1,41 @@
+---
+status: resolved
+trigger: "Resolve PR #242 review comment 6131874990"
+created: 2026-10-14
+updated: 2026-10-14
+---
+
+# PR #242 Paja release compatibility
+
+## Requirements
+
+Resolve [the review follow-up](https://github.com/napplet/web/pull/242#issuecomment-6131874990): make the conformance CLI release install with compatible Paja peers, and preserve the GAMEPAD subscription `id` rejection and silent reference-shell drop required by the living proposal.
+
+## Evidence and decision
+
+- npm now publishes Paja 0.21.0 (published 2026-10-13T01:38:08.664Z). Its core/NAP peers are `>=0.32.0 <0.35.0`, admitting this PR's 0.34.0 packages. Paja 0.20.1 peers remain `>=0.32.0 <0.33.0`.
+- The branch permits `>=0.20.1 <0.22.0` and still locks 0.20.1. Publication alone does not remove that incompatible locked dependency.
+- Keep the planned core/NAP 0.34.0 release and require the already-published Paja 0.21.x line. Update the lockfile and docs. Include the correction in the prepared, unpublished conformance CLI 0.3.4 release.
+- Current [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108) head `0b28f77e3dfa0f07dd7c0f9d61a7090ee99f3a5e`, Wire Protocol and Error Handling, defines messages without correlation IDs and silent handling of malformed subscriptions. Current NIP-5D PR #2303 head is `020cb8b33a9e4c6b8ca4b2f9d0ed0a67843b68f7`. Keep the existing validator and reference-shell behavior.
+
+## Verification plan
+
+1. Reproduce the peer conflict with packed core/NAP 0.34.0 and published Paja 0.20.1 using strict peer installation.
+2. Pack the updated CLI and its unpublished workspace dependencies; install those exact tarballs with npm strict peers and published Paja 0.21.0. Verify resolved package versions and run the installed CLI.
+3. Run build, type-check, recursive unit tests, conformance/browser checks, and AI-slop. Confirm both subscribe and unsubscribe reject IDs and are silently dropped by the reference shell.
+4. Push the reviewed fix, record the evidence on the PR, and inspect current CI results. No merge or publication is needed to resolve this review comment.
+
+## Resolution and verification
+
+Raised the Paja minimum to `>=0.21.0 <0.22.0`; the lockfile selects 0.21.0 and the compatible Kehto peer graph. Updated the package README, site guide, and the prepared 0.3.4 changelog. The existing `workspace:*` core/NAP dependencies and GAMEPAD validators are preserved.
+
+- Reproduced the old Paja 0.20.1/core 0.34.0 conflict with npm strict peers: `ERESOLVE` and the incompatible `<0.33.0` peer range.
+- Packed the actual core 0.34.0, NAP 0.34.0, conformance 0.20.0, and conformance CLI 0.3.4 builds with `pnpm pack`; verified that packed manifests contain normal version dependencies, not workspace protocols.
+- A temporary loopback registry served these four unchanged, unpublished tarballs and redirected all other packages to npm. Fresh consumer manifests depended only on conformance CLI 0.3.4. `npm install --strict-peer-deps` and `pnpm install --strict-peer-dependencies` passed without overrides, `--force`, or legacy peer handling. Dependency trees resolved core/NAP 0.34.0 throughout.
+- npm initially selected published Paja 0.21.0. Paja 0.21.1 was published during verification; an initial tarball 404 cleared, after which isolated npm and pnpm installs selected 0.21.1 and passed. Both releases retain compatible core/NAP peers.
+- The installed CLI captured 640×360 PNGs with both Paja 0.21.0 and 0.21.1 after a fixture received its first GAMEPAD state callback. No GUI browser was opened.
+- Direct assertions against the built conformance package accepted valid subscribe/unsubscribe envelopes, rejected an `id` on both, and verified silent reference-shell drops. No protocol behavior changed.
+- Build: 12 tasks passed. Types: 18 tasks passed. Recursive unit tests passed. Conformance browser suite: 5 tests passed. Release-tooling and workflow contract tests passed. Frozen workspace install with strict peers and whitespace checks passed.
+- AI-slop: 98/100, zero errors, only the existing `fflate` dependency advisory.
+
+The fix is committed as `3c3f1a66` and pushed to PR #242. Local verification logs and packed-install evidence are under `/tmp/napplet-pr242-*`; the scratch registry root is `/tmp/napplet-pr242-peers.a32qMw`. These tests establish release compatibility without claiming the napplet package set is already on npm. The PR check runs and review response carry the remote verification record. No merge or publication was performed.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/.gitkeep b/.planning/phases/163-nap-gamepad-across-repositories/.gitkeep
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-01-PLAN.md b/.planning/phases/163-nap-gamepad-across-repositories/163-01-PLAN.md
new file mode 100644
index 00000000..bfd11fcd
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-01-PLAN.md
@@ -0,0 +1,21 @@
+---
+phase: 163-nap-gamepad-across-repositories
+plan: 01
+type: execute
+wave: 01
+depends_on: []
+autonomous: true
+requirements: [GP-API, GP-WIRE, GP-TRUST]
+---
+
+# Portable API and injection
+
+Non-normative execution plan; all protocol choices defer to the living references in 163-CONTEXT.md.
+
+
+Add core schemas/API/domain identity; NAP types, cached shim, SDK helpers and exports; runtime injection and sdk namespace/subpath; exact wire and lifecycle tests. Verify through the runtime-provided namespace before expanding to runtime consumers.
+
+
+
+Verify the named requirements against current source, built import surfaces and task-relevant unit/browser tests. Record actual commands/results and unresolved specification conflicts. Preserve unrelated work. Commit each green checkpoint. Do not claim completion from import presence alone; prove the live/neutral input boundary and teardown.
+
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-01-SUMMARY.md b/.planning/phases/163-nap-gamepad-across-repositories/163-01-SUMMARY.md
new file mode 100644
index 00000000..5ad2d1b0
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-01-SUMMARY.md
@@ -0,0 +1,11 @@
+---
+phase: 163
+plan: 01
+status: complete
+---
+
+# Portable GAMEPAD API
+
+Core types/domain, NAP binding and validators, SDK root/subpath exports, npm/JSR maps, and selected shim installation are implemented. Built export tests, cached snapshot/range/parent-source tests, full build/type/unit gates and slop pass.
+
+Non-normative evidence; protocol authority remains the living references in 163-CONTEXT.md.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-02-PLAN.md b/.planning/phases/163-nap-gamepad-across-repositories/163-02-PLAN.md
new file mode 100644
index 00000000..453eec6a
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-02-PLAN.md
@@ -0,0 +1,21 @@
+---
+phase: 163-nap-gamepad-across-repositories
+plan: 02
+type: execute
+wave: 02
+depends_on: [163-01]
+autonomous: true
+requirements: [GP-ISOLATION, GP-WIRE, GP-TRUST]
+---
+
+# Runtime isolation and conformance
+
+Non-normative execution plan; all protocol choices defer to the living references in 163-CONTEXT.md.
+
+
+Implement shell-side focus-scoped polling/snapshots in Kehto and its Paja/host consumers. Add reference harness state behavior, exact envelope validation, native controller denial and real Chromium multi-napplet focus/visibility/unsubscribe tests. Keep physical device handles outside napplets.
+
+
+
+Verify the named requirements against current source, built import surfaces and task-relevant unit/browser tests. Record actual commands/results and unresolved specification conflicts. Preserve unrelated work. Commit each green checkpoint. Do not claim completion from import presence alone; prove the live/neutral input boundary and teardown.
+
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-02-SUMMARY.md b/.planning/phases/163-nap-gamepad-across-repositories/163-02-SUMMARY.md
new file mode 100644
index 00000000..f904e272
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-02-SUMMARY.md
@@ -0,0 +1,11 @@
+---
+phase: 163
+plan: 02
+status: complete
+---
+
+# Runtime and conformance
+
+Conformance implements the three envelopes and cached parent-bound browser binding. Kehto provides host-owned native reading, input-owner/visibility isolation, controller Permissions-Policy denial, subscription/rate/session cleanup, and optional SHELL-independent routing. Chromium proves native denial and frame/host/tab focus isolation with canonical host test samples. Native API replacement remains deferred for the disclosed specification conflict.
+
+Non-normative evidence; protocol authority remains the living references in 163-CONTEXT.md.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-03-PLAN.md b/.planning/phases/163-nap-gamepad-across-repositories/163-03-PLAN.md
new file mode 100644
index 00000000..8ec83dbc
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-03-PLAN.md
@@ -0,0 +1,21 @@
+---
+phase: 163-nap-gamepad-across-repositories
+plan: 03
+type: execute
+wave: 03
+depends_on: [163-02]
+autonomous: true
+requirements: [GP-REPOS, GP-DOCS, GP-SHIP]
+---
+
+# All repositories, docs and shipment
+
+Non-normative execution plan; all protocol choices defer to the living references in 163-CONTEXT.md.
+
+
+Audit all seven organization repos plus Kehto and every package. Add applicable generator/client/template integrations and evidence for unchanged surfaces. Sync package/site/skill/boilerplate docs, preserve keyboard/pointer fallbacks, run required gates, prepare release metadata and open PRs for each changed repo.
+
+
+
+Verify the named requirements against current source, built import surfaces and task-relevant unit/browser tests. Record actual commands/results and unresolved specification conflicts. Preserve unrelated work. Commit each green checkpoint. Do not claim completion from import presence alone; prove the live/neutral input boundary and teardown.
+
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-03-SUMMARY.md b/.planning/phases/163-nap-gamepad-across-repositories/163-03-SUMMARY.md
new file mode 100644
index 00000000..f9e85cd9
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-03-SUMMARY.md
@@ -0,0 +1,15 @@
+---
+phase: 163
+plan: 03
+status: complete
+---
+
+# All repositories and shipment
+
+All seven organization repositories plus Kehto are audited. Applicable source, consumer/toolchain, template, migration, policy, organization, site/package/skill, and release surfaces are covered in COVERAGE.md. Primary build/type/unit/docs/slop gates pass. CVM controller suites pass; protected-authority and timing failures also reproduce on its untouched baseline. Linked PRs and final host gates are recorded before completion.
+
+Non-normative evidence; protocol authority remains the living references in 163-CONTEXT.md.
+
+Opened PRs: web #242, Kehto #297, boilerplate #7, Nappagochi #1, migrate #1, CVM #1, organization #1. naps proposal #108 remains the canonical open draft. Publish Kehto CLI 0.5.5/Paja 0.21.0 before the primary release. All owned working trees are clean; unrelated original work remains unchanged.
+
+Release sequencing correction: primary web #242 publishes the SDK/conformance set while retaining the already-published CLI runners. Draft follow-up [#243](https://github.com/napplet/web/pull/243) changes the private runner pins and CLI version only after conformance CLI 0.3.4 and Kehto CLI 0.5.5/Paja 0.21.0 publish. Fresh-cache CI correctly rejected nonexistent future versions, which prompted this split; no provisioning test was weakened.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-CONTEXT.md b/.planning/phases/163-nap-gamepad-across-repositories/163-CONTEXT.md
new file mode 100644
index 00000000..55f970d6
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-CONTEXT.md
@@ -0,0 +1,19 @@
+# Phase 163 context
+
+Non-normative implementation planning. The living sources govern; this document creates no protocol requirements.
+
+User objective: implement NAP-GAMEPAD with coverage across every napplet repository and package, including boilerplate. Canonical proposal: https://github.com/napplet/naps/pull/108 (current head inspected: 0b28f77e3dfa0f07dd7c0f9d61a7090ee99f3a5e). Canonical NIP-5D: https://github.com/nostr-protocol/nips/pull/2303 (current head inspected: 020cb8b33a9e4c6b8ca4b2f9d0ed0a67843b68f7). Recheck heads before shipping.
+
+Implementation acceptance references:
+
+- GP-API: implement the four operations and initial cached state from NAP-GAMEPAD API Surface, with exported types and npm/JSR subpaths.
+- GP-WIRE: implement exactly the three fire-and-forget envelopes from Wire Protocol; reject malformed state without adding requests, results or ids.
+- GP-ISOLATION: implement Shell Behavior focus, neutral snapshots, visibility, connections, unavailable reasons, unsubscribe and session teardown; test native denial in a real browser.
+- GP-TRUST: preserve NIP-5D Transport parent/source checks, domain presence and injection timing.
+- GP-REPOS: audit every napplet organization repo and every web monorepo package; implement relevant integration and document evidence for surfaces needing no change. Include Kehto because it supplies runtime/Paja services consumed by these packages.
+- GP-DOCS: update package, site, skills and boilerplate guidance with optional availability, focus transitions and keyboard/pointer fallback.
+- GP-SHIP: meaningful unit/browser/package export tests; required repository build/type/unit/doc/slop checks; changesets/release metadata; atomic commits and open PRs for changed repos.
+
+Native API replacement is an unresolved spec conflict: NAP-GAMEPAD Web Projection Guidance permits navigator.getGamepads replacement, whereas current NIP-5D Security Rationale item 5 limits runtime injection to window.napplet. Preserve the native operation and deny physical access through Permissions-Policy; the required domain API does not need that optional replacement. Surface this conflict in PRs and coverage evidence.
+
+Existing unrelated work in the original worktrees remains untouched. Work happens on feat/nap-gamepad in separate clones/worktrees based on each repository default branch.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-RESEARCH.md b/.planning/phases/163-nap-gamepad-across-repositories/163-RESEARCH.md
new file mode 100644
index 00000000..9558f59b
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-RESEARCH.md
@@ -0,0 +1,11 @@
+# Research
+
+Non-normative; defer to the linked living specifications in CONTEXT.md.
+
+The active web monorepo separates protocol/global types in core, per-domain types/shim/helpers in nap, selected runtime injection in shim, and late-bound application wrappers in sdk. npm/JSR export maps and tsup entrypoints require explicit new subpaths. Existing core dispatch registration is private package plumbing; do not add wire handshakes.
+
+Conformance has JSON envelope validators, endpoint-bound reference responses, a browser boot harness and separate CLI/Paja transport. Add state/subscription coverage and controller denial to the existing boundaries. The browser harness presently creates allow-scripts frames without controller policy denial. Kehto owns production runtime/Paja device and focus mediation and therefore needs corresponding integration.
+
+Organization inventory: web, naps, boilerplate, .github, cvm, nappagochi, migrate. naps contains published GAMEPAD proposal #108; .github is community metadata. Audit other repos against their current default branches, not dirty existing local worktrees. Boilerplate documentation contains obsolete NIP-5A aggregate-hash guidance; correct it against current NIP-5D while adding controller authoring guidance.
+
+Verification will use canonical connected/neutral/blocked examples, multi-session focus transitions and native Permissions-Policy denial in Chromium. Test public import surfaces in built npm packages and JSR source maps. Do not require controllers on machines that lack them or declare gamepad as a hard capability for apps retaining other input.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-REVIEW.md b/.planning/phases/163-nap-gamepad-across-repositories/163-REVIEW.md
new file mode 100644
index 00000000..52cfdd7f
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-REVIEW.md
@@ -0,0 +1,15 @@
+---
+phase: 163
+status: passed
+review_mode: inline
+---
+
+# Implementation review
+
+Reviewed the changed API, serialized bindings, source/session routing, host broker, frame policies, manifest inference, consumer ranges, release metadata, and documentation against the current canonical sources. No subagents or external reviewers were invoked. No unresolved GAMEPAD implementation finding remains in the reviewed scope.
+
+The review preserved existing public host registration contracts: trusted sessions registered directly by integrators also route independently of SHELL readiness, without trusting app payload identity. Regression coverage verifies this. Paja's retained handshake-domain export is empty and SHELL is an optional advertised interface. Every controller frame is denied native gamepad/HID/USB paths before execution.
+
+The optional native Gamepad API replacement remains deferred due to the disclosed NIP-5D/GAMEPAD conflict. CVM's existing protected-authority baseline failures remain visible. Release ordering is explicit: Kehto CLI 0.5.5 before napplet CLI 0.9.1; the napplet set contains SDK 0.30.0 and conformance CLI 0.3.4. CLI smoke uses locally assembled source-build cache entries and an empty host-tool PATH; it does not claim those future versions are already published.
+
+Release sequencing correction: primary web #242 publishes the SDK/conformance set while retaining the already-published CLI runners. Draft follow-up [#243](https://github.com/napplet/web/pull/243) changes the private runner pins and CLI version only after conformance CLI 0.3.4 and Kehto CLI 0.5.5/Paja 0.21.0 publish. Fresh-cache CI correctly rejected nonexistent future versions, which prompted this split; no provisioning test was weakened.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-SECURITY.md b/.planning/phases/163-nap-gamepad-across-repositories/163-SECURITY.md
new file mode 100644
index 00000000..15cf70ab
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-SECURITY.md
@@ -0,0 +1,22 @@
+---
+phase: 163
+status: passed
+threats_open: 0
+verified: 2026-10-12
+---
+
+# GAMEPAD security review
+
+Non-normative code review against the living [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108) Shell Behavior/Security Considerations and [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) Transport/Security Rationale. Canonical heads rechecked unchanged on 2026-10-12: GAMEPAD 0b28f77e3dfa0f07dd7c0f9d61a7090ee99f3a5e; NIP-5D 020cb8b33a9e4c6b8ca4b2f9d0ed0a67843b68f7.
+
+| Threat | Implemented boundary | Proof |
+| --- | --- | --- |
+| Untrusted state injection | Portable, reference, and Kehto bindings require parent message source and validate state before caching. | Parent/sibling trust regressions; malformed state/id/range tests. |
+| Cross-frame live controller leakage | Runtime recipient identity comes from the registered Window; live controls require trusted input ownership and active host focus/visibility. | Two-window broker vectors; Chromium iframe focus transfer and host/tab focus loss. |
+| Timing leakage through neutral state | Unfocused views retain connected metadata with axes/buttons neutral and timestamp zero; unchanged views do not receive repeated live timestamps. | Broker unit vectors and Chromium snapshots. |
+| Native bypass | Every Paja/playground loading path applies controller Permissions-Policy denial before execution. Reference iframes deny gamepad, HID, and USB too. | Real opaque Chromium frames throw SecurityError on native getGamepads; policy preservation/idempotency regression. |
+| Mutating shared cached state | Bindings copy nested arrays/buttons for callers and each listener. | Cache/fanout mutation regressions. |
+| Polling/subscription abuse or teardown leak | Shared frame loop, per-source repeated-subscribe limit, unconditional unsubscribe, generation cancellation, session/service cleanup. | Broker frame cancellation, rate limit, stale callback, destruction and unregister regressions. |
+| Fabricated identity/authority handshake | Source identity and frozen grants are host-owned and independent of optional SHELL readiness. No new capability or message is introduced. | Immediate subscribe without SHELL; unknown-source and disabled-domain routing tests. |
+
+The optional native Gamepad API replacement is not implemented. The proposal permits it while NIP-5D currently limits runtime injection to window.napplet; the conflict is surfaced for clarification. Required domain support and physical-access denial do not depend on it. No cloud deployment, account authority, key material, haptics, or raw device handles are introduced by this change.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/163-VERIFICATION.md b/.planning/phases/163-nap-gamepad-across-repositories/163-VERIFICATION.md
new file mode 100644
index 00000000..4c325216
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/163-VERIFICATION.md
@@ -0,0 +1,27 @@
+---
+phase: 163
+status: passed
+verified: 2026-10-12
+---
+
+# GAMEPAD verification
+
+Non-normative implementation evidence against the current living NAP-GAMEPAD and NIP-5D sources linked in CONTEXT.md. Canonical heads and organization inventory were rechecked unchanged.
+
+| Acceptance reference | Proof | Result |
+| --- | --- | --- |
+| GP-API | Core types/domain, NAP binding, built SDK root/subpath import tests, npm/JSR exports, selected shim, synchronous cached access and independent listeners. | Passed |
+| GP-WIRE | Three message discriminants/directions, no correlation ids, full state, ranges, mappings, null slots, unavailable and neutral views; reference mock and serialized injection tests. | Passed |
+| GP-ISOLATION | Kehto broker units and Chromium prove live/neutral frame isolation, host/tab focus loss, native controller denial, connection updates, polling/rate/session cleanup. | Passed |
+| GP-TRUST | Parent-source checks, trusted creation/prebound sessions, frozen domain environment, disabled-domain/unknown-source rejection, no SHELL prerequisite. | Passed |
+| GP-REPOS | All seven organization repos, all web packages/apps/fixtures, and Kehto runtime dependencies inventoried in COVERAGE.md; appropriate changes and explicit unchanged surfaces recorded. | Passed |
+| GP-DOCS | Package/site/skill/template guidance, consumer/migration/CVM/organization docs, current manifest schema, controller fallbacks and focus-edge guidance. | Passed |
+| GP-SHIP | Primary build/types/recursive units/docs/slop; applicable consumer gates; source-build compiled CLI screenshot/Paja proof; release metadata and linked PR shipment. | Passed with disclosed existing CVM baseline failures |
+
+Native API replacement remains deferred for the disclosed optional-projection/NIP-5D injection conflict. No new protocol surface is fabricated. CVM full-suite protected authority failures also occur on untouched main; controller-specific tests pass. Kehto's prospective 0.34 package matrix deliberately uses temporary packed source builds; distribution guards reject those overrides, which are never committed. Release order remains Kehto CLI 0.5.5/Paja 0.21.0 before the napplet package set. No production deployment or image promotion is performed.
+
+Final shipment review: SDK/tooling https://github.com/napplet/web/pull/242 and runtime https://github.com/kehto/web/pull/297 are open, with all five consumer/community PRs linked in COVERAGE.md. Reviewed the final shipment summary after these links were added; source and verification results are unchanged.
+
+Release sequencing correction: primary web #242 publishes the SDK/conformance set while retaining the already-published CLI runners. Draft follow-up [#243](https://github.com/napplet/web/pull/243) changes the private runner pins and CLI version only after conformance CLI 0.3.4 and Kehto CLI 0.5.5/Paja 0.21.0 publish. Fresh-cache CI correctly rejected nonexistent future versions, which prompted this split; no provisioning test was weakened.
+
+Reviewed the final split-release shipment summary: all eight implementation/consumer/follow-up PRs are open; original worktrees remain untouched. Required local source gates and assembled release-build controller proof remain as recorded above.
diff --git a/.planning/phases/163-nap-gamepad-across-repositories/COVERAGE.md b/.planning/phases/163-nap-gamepad-across-repositories/COVERAGE.md
new file mode 100644
index 00000000..ae5e8762
--- /dev/null
+++ b/.planning/phases/163-nap-gamepad-across-repositories/COVERAGE.md
@@ -0,0 +1,49 @@
+# GAMEPAD coverage
+
+Non-normative implementation evidence for phase 163. The living [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108) and [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) govern protocol behavior.
+
+## Organization repositories
+
+| Repository | Work and evidence |
+| --- | --- |
+| napplet/web | Cached NAP binding, core types, SDK subpaths, shim injection, manifest inference, conformance envelopes/reference injection, managed CLI pins, site/package/skill docs, release metadata. |
+| napplet/naps | PR 108 is the canonical proposed contract; implementation follows its actual head. No duplicate or private normative specification is added. |
+| napplet/boilerplate | Optional controller presence row and guide, updated SDK/toolchain compatibility, current manifest schema, single-file build, type/guidance/browser conformance tests. |
+| napplet/cvm | GAMEPAD generation evidence/schema, required/optional declaration checks, publication compatibility regression, toolchain promotion guidance. Existing protected-authority baseline failures remain surfaced. |
+| napplet/nappagochi | SDK/Paja/toolchain compatibility updates; controller support stays optional and existing interactions and pet lifecycle remain intact. Existing app unit/build/type/browser conformance checks. |
+| napplet/migrate | Regression proves required/optional GAMEPAD domain preservation without adding a domain whitelist; migration guidance, full types/tests/build. |
+| napplet/.github | Organization repository inventory and controller guidance; retired app-owned shim/social relay examples corrected. No executable controller surface. |
+| kehto/web (runtime dependency) | Host broker, focus/visibility reader, Permissions-Policy denial across Paja/playground frames, cached parent-bound namespace, source sessions independent of SHELL, lifecycle/rate controls, units and Chromium focus/native-denial proof. |
+
+## Package and fixture coverage
+
+| Package | Surface | Coverage |
+| --- | --- | --- |
+| `@napplet/core` | `packages/core` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/vite-plugin` | `packages/vite-plugin` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/cli` | `packages/cli` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/sdk` | `packages/sdk` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/boilerplate` | `packages/boilerplate` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet/shim` | `packages/shim` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/nap` | `packages/nap` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/conformance-cli` | `packages/conformance-cli` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/conformance` | `packages/conformance` | Implementation, exports, tests, docs and release metadata. |
+| `@napplet/web` | `apps/web` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet/docs` | `apps/docs` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet/conformance-web` | `apps/conformance` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet/conformance-e2e` | `tests/e2e/harness` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet-fixture/broken` | `tests/fixtures/napplets/broken` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet-fixture/conformant` | `tests/fixtures/napplets/conformant` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+| `@napplet-fixture/resource-data` | `tests/fixtures/napplets/resource-data` | Existing build/tests or browser consumers audited; no new hard controller requirement. See repository proof below. |
+
+## Release ordering and limits
+
+Publish Kehto CLI 0.5.5/Paja 0.21.0 and the primary conformance CLI 0.3.4 release before [CLI runner follow-up #243](https://github.com/napplet/web/pull/243). The follow-up is a draft stacked PR that prepares CLI 0.9.1; the primary release retains CLI 0.9.0 and its published private runners to avoid a self-publication cycle. The napplet package set includes conformance CLI 0.3.4 and SDK 0.30.0. Consumer ranges accept the currently published base release and the coordinated GAMEPAD release; installing the base release does not supply new SDK exports. CVM's sealed image remains subject to its existing candidate-promotion gates; this change does not promote a production image.
+
+Native Gamepad API replacement remains deferred: the GAMEPAD proposal permits an optional replacement while NIP-5D currently limits runtime injection to window.napplet. Required domain support and physical-access denial do not depend on that replacement. Reference mocks intentionally report unavailable controllers; live host isolation is demonstrated separately in Kehto Chromium tests with test-supplied host samples.
+
+Verified primary `pnpm build`, `pnpm type-check`, `pnpm -r test:unit`, docs build and slop 100/100. Compiled macOS CLI rendered the controller page and ran Paja help using locally assembled release builds with no host runtime/package runner on PATH. The first screenshot exposed the obsolete Paja dependency; the corrected source build renders Controller available. Boilerplate verify/conformance and eight size/theme contexts pass. Nappagochi verify, 88 tests and conformance pass. Migration check/build and 40 tests pass. CVM's 15 controller tests and slop pass; its existing protected-authority/admission baseline failures are disclosed in its PR. Kehto build/types/docs/slop, controller browser proof and baseline unit suite pass; core/NAP 0.34 tarballs also pass build/types and 2105 behavior tests while distribution-only guards reject temporary unpublished overrides. Those overrides are removed before shipment.
+
+Opened consumer PRs: [boilerplate #7](https://github.com/napplet/boilerplate/pull/7), [Nappagochi #1](https://github.com/napplet/nappagochi/pull/1), [migrate #1](https://github.com/napplet/migrate/pull/1), [CVM #1](https://github.com/napplet/cvm/pull/1), [organization #1](https://github.com/napplet/.github/pull/1). Primary SDK/tooling [web #242](https://github.com/napplet/web/pull/242) and runtime [Kehto #297](https://github.com/kehto/web/pull/297) are open. Kehto final baseline verification passes all 2110 tests; build/types/docs/slop 100/100 pass. Original dirty worktrees and unrelated changes remain untouched.
+
+Release sequencing correction: primary web #242 publishes the SDK/conformance set while retaining the already-published CLI runners. Draft follow-up [#243](https://github.com/napplet/web/pull/243) changes the private runner pins and CLI version only after conformance CLI 0.3.4 and Kehto CLI 0.5.5/Paja 0.21.0 publish. Fresh-cache CI correctly rejected nonexistent future versions, which prompted this split; no provisioning test was weakened.
diff --git a/README.md b/README.md
index 1b45df35..9d8ed4a5 100644
--- a/README.md
+++ b/README.md
@@ -190,3 +190,23 @@ pnpm --filter @napplet/conformance-web dev # conformance runtime
## License
MIT
+
+### 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.
diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts
index 4f408d41..92c049a9 100644
--- a/apps/docs/.vitepress/config.ts
+++ b/apps/docs/.vitepress/config.ts
@@ -91,7 +91,7 @@ export default defineConfig({
},
{
text: 'NAP domains',
- items: [{ text: 'NAP domain reference', link: '/naps/' }],
+ items: [{ text: 'NAP domain reference', link: '/naps/' }, { text: 'GAMEPAD', link: '/naps/gamepad' }],
},
{
text: 'Reference',
diff --git a/apps/docs/guide/build-note-drafts-napplet.md b/apps/docs/guide/build-note-drafts-napplet.md
index bdd598a9..9bab8c32 100644
--- a/apps/docs/guide/build-note-drafts-napplet.md
+++ b/apps/docs/guide/build-note-drafts-napplet.md
@@ -41,7 +41,7 @@ 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.29.0
+pnpm add @napplet/sdk@^0.30.0
pnpm add -D @napplet/vite-plugin@^0.16.0 @napplet/conformance-cli@^0.3.0 @kehto/cli@^0.2.11 typescript@^5.9.3 vite@^6.4.3
```
@@ -62,7 +62,7 @@ Replace the generated `package.json` with this:
"verify": "pnpm type-check && pnpm test:conformance"
},
"dependencies": {
- "@napplet/sdk": "^0.29.0"
+ "@napplet/sdk": "^0.30.0"
},
"devDependencies": {
"@kehto/cli": "^0.2.11",
diff --git a/apps/docs/naps/gamepad.md b/apps/docs/naps/gamepad.md
new file mode 100644
index 00000000..11204436
--- /dev/null
+++ b/apps/docs/naps/gamepad.md
@@ -0,0 +1,21 @@
+# GAMEPAD
+
+### 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.
diff --git a/apps/docs/naps/index.md b/apps/docs/naps/index.md
index 51cc80c3..eb5310ff 100644
--- a/apps/docs/naps/index.md
+++ b/apps/docs/naps/index.md
@@ -267,7 +267,7 @@ if (window.napplet?.fs) {
## Core domain union
-[`@napplet/core`](/packages/core) 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` — used as the discriminant for envelope routing and domain presence.
+[`@napplet/core`](/packages/core) 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` — used as the discriminant for envelope routing and domain presence.
## Where to go next
@@ -293,3 +293,7 @@ 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).
+
+### gamepad
+
+Optional focus-scoped controller snapshots. See the non-normative [GAMEPAD guide](./gamepad) and living [NAP-GAMEPAD proposal](https://github.com/napplet/naps/pull/108).
diff --git a/apps/docs/packages/conformance-cli.md b/apps/docs/packages/conformance-cli.md
index b258f551..48d16e5b 100644
--- a/apps/docs/packages/conformance-cli.md
+++ b/apps/docs/packages/conformance-cli.md
@@ -56,3 +56,5 @@ source edits rebuild and re-check automatically.
- [`@napplet/vite-plugin`](./vite-plugin) — 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](./cli.md#screenshots-and-optional-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.
diff --git a/apps/docs/packages/core.md b/apps/docs/packages/core.md
index 0b77fac0..fe835bb3 100644
--- a/apps/docs/packages/core.md
+++ b/apps/docs/packages/core.md
@@ -32,7 +32,7 @@ import {
- **`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'`).
+ (`'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'`).
- **`NappletGlobal`** — the runtime-injected `window.napplet` namespace with
optional domain properties.
- **`NAP_DOMAINS`** — runtime constant array of all domain strings, for iteration
diff --git a/apps/docs/packages/nap.md b/apps/docs/packages/nap.md
index f7e94a61..901e19a0 100644
--- a/apps/docs/packages/nap.md
+++ b/apps/docs/packages/nap.md
@@ -152,4 +152,23 @@ intent.onDelivery(({ 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).
\ No newline at end of file
+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.
diff --git a/apps/docs/packages/sdk.md b/apps/docs/packages/sdk.md
index a830b3ea..131d7508 100644
--- a/apps/docs/packages/sdk.md
+++ b/apps/docs/packages/sdk.md
@@ -153,3 +153,23 @@ 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.
diff --git a/packages/boilerplate/README.md b/packages/boilerplate/README.md
index c5537215..6405299f 100644
--- a/packages/boilerplate/README.md
+++ b/packages/boilerplate/README.md
@@ -56,3 +56,7 @@ npx skills add napplet/napplet
```
Treat the [living NIP-5D proposal](https://github.com/nostr-protocol/nips/pull/2303), [NAP-OUTBOX](https://github.com/napplet/naps/pull/32), and [NAP-RELAY](https://github.com/napplet/naps/pull/2) as protocol authority for the claims below. The shipped skills provide non-normative authoring guidance: the runtime injects `window.napplet`, app calls use `@napplet/sdk`, direct domain-property checks are only for optional fallbacks, normal Nostr reads and publishes are OUTBOX-first, RELAY is an explicit relay-local escape hatch, and manifest `requires` lists hard requirements only.
+
+### GAMEPAD integration
+
+Generated projects keep controller input optional unless it is essential to the app’s core task. The template’s controller guide covers fallbacks, focus edges, and runtime-owned native access. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108).
diff --git a/packages/cli/README.md b/packages/cli/README.md
index ef853869..0275ea06 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -446,3 +446,7 @@ The opt-in integration test compiles the release entrypoint and runs with an emp
```sh
NAPPLET_TEST_STANDALONE=1 deno test --node-modules-dir=none -A packages/cli/tests/standalone_integration_test.ts
```
+
+### GAMEPAD integration
+
+GAMEPAD is a bare manifest domain accepted by `init`, `build`, and deployment metadata. Controller fallback apps use `O gamepad`; controller-required apps use `R gamepad`. Live `dev` controller input depends on a GAMEPAD-capable Kehto/Paja release. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108).
diff --git a/packages/conformance-cli/CHANGELOG.md b/packages/conformance-cli/CHANGELOG.md
index 7be54e40..dd427608 100644
--- a/packages/conformance-cli/CHANGELOG.md
+++ b/packages/conformance-cli/CHANGELOG.md
@@ -1,5 +1,16 @@
# @napplet/conformance-cli
+## 0.3.4
+
+### Patch Changes
+
+- Require Paja 0.21.x, whose published peer range supports core/NAP 0.34.0, and update the lockfile to remove the incompatible Paja 0.20.1 resolution.
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+- Updated dependencies
+ - @napplet/core@0.34.0
+ - @napplet/nap@0.34.0
+ - @napplet/conformance@0.20.0
+
## 0.3.3
### Patch Changes
diff --git a/packages/conformance-cli/README.md b/packages/conformance-cli/README.md
index ef94e5e5..79bdeab0 100644
--- a/packages/conformance-cli/README.md
+++ b/packages/conformance-cli/README.md
@@ -66,3 +66,9 @@ Exit codes: `0` conformant, `1` non-conformant, `2` usage/runtime error.
## Capture a preview screenshot
The CLI also provides the browser runner for `napplet screenshot`. Run `npx @napplet/conformance-cli@0.3.3 screenshot --output preview.png` from a built project; it prefers `dist/index.html` and starts a temporary Kehto Paja preview automatically. Supply a project directory or an HTTP(S) shell URL to override the target. Use `--selector` to select one iframe, `--ready-selector` to wait for an element inside it, and `--width`, `--height`, or `--delay` to adjust the default 1200 × 750 capture after 1500 ms. Existing output files are preserved. Chromium is installed automatically on first use. Local previews use Paja's development identity and live relays, and close after capture or failure.
+
+### GAMEPAD integration
+
+Controller artifacts use the engine’s GAMEPAD envelope validation and injected cached API. The reference mock reports unavailable controllers; validate live focus isolation in a compatible runtime. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108).
+
+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.
diff --git a/packages/conformance-cli/package.json b/packages/conformance-cli/package.json
index b10668c5..9ca09452 100644
--- a/packages/conformance-cli/package.json
+++ b/packages/conformance-cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@napplet/conformance-cli",
- "version": "0.3.3",
- "description": "Headless CLI that runs @napplet/conformance against a napplet in real Chromium via Playwright — the `test:conformance` runner",
+ "version": "0.3.4",
+ "description": "Headless CLI that runs @napplet/conformance against a napplet in real Chromium via Playwright \u2014 the `test:conformance` runner",
"type": "module",
"bin": {
"napplet-conformance": "./dist/cli.js"
@@ -14,10 +14,10 @@
"access": "public"
},
"dependencies": {
- "@kehto/paja": "0.19.0",
+ "@kehto/paja": ">=0.21.0 <0.22.0",
"@napplet/conformance": "workspace:^",
- "@napplet/core": "0.32.0",
- "@napplet/nap": "0.32.0",
+ "@napplet/core": "workspace:*",
+ "@napplet/nap": "workspace:*",
"playwright": "^1.59.1"
},
"devDependencies": {
diff --git a/packages/conformance/CHANGELOG.md b/packages/conformance/CHANGELOG.md
index 010c6fb1..325d6bb1 100644
--- a/packages/conformance/CHANGELOG.md
+++ b/packages/conformance/CHANGELOG.md
@@ -1,5 +1,17 @@
# @napplet/conformance
+## 0.20.0
+
+### Minor Changes
+
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+
+### Patch Changes
+
+- Updated dependencies
+ - @napplet/core@0.34.0
+ - @napplet/nap@0.34.0
+
## 0.19.0
### Minor Changes
diff --git a/packages/conformance/README.md b/packages/conformance/README.md
index ad1c6f30..9d54dcb8 100644
--- a/packages/conformance/README.md
+++ b/packages/conformance/README.md
@@ -100,3 +100,7 @@ for (const check of run.checks) {
## CLI Pairing
The package intentionally stays framework-agnostic. For a project-level command that launches real Chromium against a built napplet, use the companion `napplet-conformance` CLI shipped from `@napplet/conformance-cli`.
+
+### GAMEPAD integration
+
+GAMEPAD validation recognizes only `gamepad.subscribe`, `gamepad.unsubscribe`, and `gamepad.state`; correlation ids are rejected. The injected browser API shares snapshot validation with the portable binding and accepts only parent messages. The reference mock reports unavailable controllers rather than physical input. Focus and native-access isolation require host integration tests. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108).
diff --git a/packages/conformance/jsr.json b/packages/conformance/jsr.json
index 73310d37..bbe717ce 100644
--- a/packages/conformance/jsr.json
+++ b/packages/conformance/jsr.json
@@ -1,11 +1,11 @@
{
"name": "@napplet/conformance",
- "version": "0.19.0",
+ "version": "0.20.0",
"license": "MIT",
"exports": "./src/index.ts",
"imports": {
- "@napplet/core": "jsr:@napplet/core@^0.33.0",
- "@napplet/nap": "jsr:@napplet/nap@^0.33.0"
+ "@napplet/core": "jsr:@napplet/core@^0.34.0",
+ "@napplet/nap": "jsr:@napplet/nap@^0.34.0"
},
"publish": {
"include": [
diff --git a/packages/conformance/package.json b/packages/conformance/package.json
index 9a349871..fde20df0 100644
--- a/packages/conformance/package.json
+++ b/packages/conformance/package.json
@@ -1,6 +1,6 @@
{
"name": "@napplet/conformance",
- "version": "0.19.0",
+ "version": "0.20.0",
"description": "Framework-agnostic napplet protocol conformance engine — reference mock shell, per-NAP envelope validators, manifest validator, check registry, and reporters",
"type": "module",
"main": "./dist/index.js",
diff --git a/packages/conformance/src/run/boot.test.ts b/packages/conformance/src/run/boot.test.ts
index ae8fcdbe..1e98d2aa 100644
--- a/packages/conformance/src/run/boot.test.ts
+++ b/packages/conformance/src/run/boot.test.ts
@@ -59,6 +59,7 @@ describe('bootAndCollect — DOM wiring', () => {
}]);
window.dispatchEvent(new MessageEvent('message', {
+ source: window.parent,
data: {
type: 'resource.bytes.result',
id: posted[0].id,
@@ -82,6 +83,7 @@ describe('bootAndCollect — DOM wiring', () => {
}],
});
window.dispatchEvent(new MessageEvent('message', {
+ source: window.parent,
data: {
type: 'resource.bytesMany.result',
id: posted[1].id,
diff --git a/packages/conformance/src/run/boot.ts b/packages/conformance/src/run/boot.ts
index 302388e3..8d0ff2b4 100644
--- a/packages/conformance/src/run/boot.ts
+++ b/packages/conformance/src/run/boot.ts
@@ -68,12 +68,58 @@ function escapeHtmlAttr(value: string): string {
.replace(/>/g, '>');
}
+/**
+ * NAP-GAMEPAD snapshot validation and copying, written as literal browser source.
+ * The prelude must not embed `Function.prototype.toString()` output: bundler and
+ * coverage transforms can add helper references that do not exist in the iframe.
+ * `gamepad.test.ts` keeps this source equivalent to `@napplet/nap/gamepad`.
+ */
+export const GAMEPAD_STATE_TOOLS_SOURCE = `(() => {
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
+ const numberIn = (value, min, max) => typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
+ const isGamepadState = (value) => {
+ if (!record(value) || typeof value.available !== 'boolean' || typeof value.focused !== 'boolean' || !Array.isArray(value.pads)) return false;
+ if (value.available) {
+ if (value.reason !== undefined) return false;
+ } else if (value.focused || (value.reason !== 'blocked' && value.reason !== 'unavailable')) return false;
+ return Array.from(value.pads).every((pad) => {
+ if (pad === null) return true;
+ if (!record(pad) || !Number.isInteger(pad.index) || pad.index < 0 || typeof pad.id !== 'string' || typeof pad.mapping !== 'string' || pad.connected !== true) return false;
+ if (typeof pad.timestamp !== 'number' || !Number.isFinite(pad.timestamp) || !Array.isArray(pad.axes) || !Array.isArray(pad.buttons)) return false;
+ if (!value.focused && pad.timestamp !== 0) return false;
+ if (!Array.from(pad.axes).every((axis) => numberIn(axis, -1, 1) && (value.focused || axis === 0))) return false;
+ return Array.from(pad.buttons).every((button) => record(button) && numberIn(button.value, 0, 1) && typeof button.pressed === 'boolean' && typeof button.touched === 'boolean' && (value.focused || (button.value === 0 && !button.pressed && !button.touched)));
+ });
+ };
+ const copyGamepadState = (state) => {
+ const copy = { available: state.available, focused: state.focused };
+ if (state.reason !== undefined) copy.reason = state.reason;
+ copy.pads = state.pads.map((pad) => pad === null ? null : {
+ index: pad.index, id: pad.id, mapping: pad.mapping, connected: pad.connected,
+ timestamp: pad.timestamp, axes: pad.axes.slice(), buttons: pad.buttons.map((button) => ({ value: button.value, pressed: button.pressed, touched: button.touched })),
+ });
+ return copy;
+ };
+ return { isGamepadState, copyGamepadState };
+})()`;
+
export function runtimePrelude(domains: readonly string[]): string {
return `
(() => {
const domains = ${JSON.stringify(domains)};
const napplet = {};
for (const domain of domains) napplet[domain] = {};
+ const gamepadTools = ${GAMEPAD_STATE_TOOLS_SOURCE};
+ let gamepadState = { available: true, focused: false, pads: [] };
+ const gamepadHandlers = new Set();
+ // Subscribe on first use, like the nap binding, so napplets that never touch
+ // gamepad emit no gamepad traffic for checks to attribute to them.
+ let gamepadSubscribed = false;
+ const gamepadSubscribe = () => {
+ if (gamepadSubscribed) return;
+ gamepadSubscribed = true;
+ window.parent.postMessage({ type: 'gamepad.subscribe' }, '*');
+ };
const pending = new Map();
let seq = 0;
const nextId = () => {
@@ -93,7 +139,18 @@ export function runtimePrelude(domains: readonly string[]): string {
window.parent.postMessage({ ...message, id }, '*');
});
window.addEventListener('message', (event) => {
+ if (event.source !== window.parent) return;
const msg = event.data;
+ if (gamepadSubscribed && msg && msg.type === 'gamepad.state' && !('id' in msg) && gamepadTools.isGamepadState(msg)) {
+ gamepadState = gamepadTools.copyGamepadState(msg);
+ const recipients = Array.from(gamepadHandlers);
+ for (const entry of recipients) {
+ if (!gamepadHandlers.has(entry)) continue;
+ try { entry.handler(gamepadTools.copyGamepadState(gamepadState)); }
+ catch (error) { queueMicrotask(() => { throw error; }); }
+ }
+ return;
+ }
if (!msg || typeof msg !== 'object' || typeof msg.id !== 'string') return;
const slot = pending.get(msg.id);
if (!slot) return;
@@ -106,6 +163,19 @@ export function runtimePrelude(domains: readonly string[]): string {
}
slot.resolve(msg);
});
+ if (napplet.gamepad) {
+ napplet.gamepad = {
+ get available() { gamepadSubscribe(); return gamepadState.available; },
+ get focused() { gamepadSubscribe(); return gamepadState.focused; },
+ getGamepads() { gamepadSubscribe(); return gamepadTools.copyGamepadState(gamepadState).pads; },
+ onChange(handler) {
+ const entry = { handler };
+ gamepadHandlers.add(entry);
+ gamepadSubscribe();
+ return { close() { gamepadHandlers.delete(entry); } };
+ },
+ };
+ }
if (napplet.resource) {
napplet.resource.info = () =>
request({ type: 'resource.info' }, 'resource.info.result', 'resource.info.error')
@@ -190,6 +260,9 @@ async function bootOnce(
const shell = createReferenceShell();
const iframe = doc.createElement('iframe');
iframe.setAttribute('sandbox', opts.allowSameOrigin ? 'allow-scripts allow-same-origin' : 'allow-scripts');
+ // NAP-GAMEPAD Web Projection Guidance: deny the native API with Permissions-Policy.
+ // https://github.com/napplet/naps/pull/108
+ iframe.setAttribute('allow', "gamepad 'none'");
iframe.setAttribute('aria-hidden', 'true');
iframe.style.position = 'absolute';
iframe.style.width = '1px';
diff --git a/packages/conformance/src/run/gamepad.test.ts b/packages/conformance/src/run/gamepad.test.ts
new file mode 100644
index 00000000..9a1fb5f4
--- /dev/null
+++ b/packages/conformance/src/run/gamepad.test.ts
@@ -0,0 +1,103 @@
+// @vitest-environment happy-dom
+import { describe, expect, it, vi } from 'vitest';
+import type { GamepadApi } from '@napplet/core';
+import { createGamepadStateTools } from '@napplet/nap/gamepad';
+import { GAMEPAD_STATE_TOOLS_SOURCE, runtimePrelude } from './boot.js';
+
+describe('serialized GAMEPAD conformance binding', () => {
+ it('installs before subject code, accepts only parent state and isolates cached data', () => {
+ const previous = window.parent;
+ const post = vi.fn();
+ Object.defineProperty(window, 'parent', { configurable: true, value: { postMessage: post } });
+ try {
+ Function(runtimePrelude(['gamepad']))();
+ const api = (window as unknown as { napplet: { gamepad: GamepadApi } }).napplet.gamepad;
+ expect(post).not.toHaveBeenCalled();
+ expect(api.available).toBe(true);
+ expect(api.focused).toBe(false);
+ expect(api.getGamepads()).toEqual([]);
+ expect(post.mock.calls).toEqual([[{ type: 'gamepad.subscribe' }, '*']]);
+ const handler = vi.fn();
+ const sub = api.onChange(handler);
+ const state = { type: 'gamepad.state', available: false, focused: false, reason: 'blocked', pads: [] };
+ window.dispatchEvent(new MessageEvent('message', { data: state, source: {} as MessageEventSource }));
+ expect(handler).not.toHaveBeenCalled();
+ window.dispatchEvent(new MessageEvent('message', { data: state, source: window.parent }));
+ expect(api.available).toBe(false);
+ expect(handler).toHaveBeenCalledExactlyOnceWith({ available: false, focused: false, reason: 'blocked', pads: [] });
+ sub.close();
+ window.dispatchEvent(new MessageEvent('message', { data: state, source: window.parent }));
+ expect(handler).toHaveBeenCalledTimes(1);
+ } finally {
+ Object.defineProperty(window, 'parent', { configurable: true, value: previous });
+ delete (window as unknown as { napplet?: unknown }).napplet;
+ }
+ });
+
+ it('isolates a throwing onChange listener from later listeners', () => {
+ const previous = window.parent;
+ Object.defineProperty(window, 'parent', { configurable: true, value: { postMessage: vi.fn() } });
+ const reported = vi.fn();
+ vi.stubGlobal('queueMicrotask', reported);
+ try {
+ Function(runtimePrelude(['gamepad']))();
+ const api = (window as unknown as { napplet: { gamepad: GamepadApi } }).napplet.gamepad;
+ api.onChange(() => { throw new Error('listener failed'); });
+ const later = vi.fn();
+ api.onChange(later);
+ const state = { available: false, focused: false, reason: 'blocked', pads: [] };
+ window.dispatchEvent(new MessageEvent('message', { data: { type: 'gamepad.state', ...state }, source: window.parent }));
+ expect(later).toHaveBeenCalledExactlyOnceWith(state);
+ expect(reported).toHaveBeenCalledOnce();
+ } finally {
+ vi.unstubAllGlobals();
+ Object.defineProperty(window, 'parent', { configurable: true, value: previous });
+ delete (window as unknown as { napplet?: unknown }).napplet;
+ }
+ });
+});
+
+describe('literal GAMEPAD state tools source', () => {
+ // Connected and neutral examples from NAP-GAMEPAD Wire Protocol, proposal #108.
+ const pad = {
+ index: 1, id: 'Xbox Wireless Controller', mapping: 'standard', connected: true,
+ timestamp: 18235.4, axes: [0, -1, 0, 0],
+ buttons: [{ value: 1, pressed: true, touched: true }, { value: 0, pressed: false, touched: false }],
+ };
+ const focused = { available: true, focused: true, pads: [null, pad] };
+ const neutralPad = { ...pad, timestamp: 0, axes: [0, 0, 0, 0], buttons: [{ value: 0, pressed: false, touched: false }, { value: 0, pressed: false, touched: false }] };
+ const neutral = { available: true, focused: false, pads: [null, neutralPad] };
+ const vectors: unknown[] = [
+ focused, neutral, null, [], 'state',
+ { available: false, reason: 'blocked', focused: false, pads: [] },
+ { available: false, reason: 'unavailable', focused: false, pads: [] },
+ { available: false, focused: false, pads: [] },
+ { available: false, reason: 'other', focused: false, pads: [] },
+ { available: false, reason: 'blocked', focused: true, pads: [] },
+ { ...focused, reason: 'blocked' },
+ { ...focused, focused: false },
+ { ...focused, pads: [false] },
+ { ...focused, pads: [{ ...pad, mapping: 'future-layout' }] },
+ { ...focused, pads: [{ ...pad, index: -1 }] },
+ { ...focused, pads: [{ ...pad, index: 1.5 }] },
+ { ...focused, pads: [{ ...pad, connected: false }] },
+ { ...focused, pads: [{ ...pad, timestamp: Infinity }] },
+ ...[-2, 2, NaN, Infinity].map((axis) => ({ ...focused, pads: [{ ...pad, axes: [axis] }] })),
+ { ...focused, pads: [{ ...pad, buttons: [{ value: 2, pressed: true, touched: true }] }] },
+ { ...focused, pads: [{ ...pad, buttons: [{ value: 1, pressed: 'yes', touched: true }] }] },
+ { ...neutral, pads: [{ ...neutralPad, timestamp: 1 }] },
+ { ...neutral, pads: [{ ...neutralPad, axes: [0.5] }] },
+ { ...neutral, pads: [{ ...neutralPad, buttons: [{ value: 0, pressed: true, touched: false }] }] },
+ ];
+
+ it('is self-contained and matches @napplet/nap/gamepad on spec vectors', () => {
+ const literal = Function(`return ${GAMEPAD_STATE_TOOLS_SOURCE};`)() as ReturnType;
+ const reference = createGamepadStateTools();
+ for (const value of vectors) expect(literal.isGamepadState(value)).toBe(reference.isGamepadState(value));
+ for (const state of [focused, neutral, { available: false, reason: 'blocked', focused: false, pads: [] }] as const) {
+ const copy = literal.copyGamepadState(state as never);
+ expect(copy).toEqual(reference.copyGamepadState(state as never));
+ if (state.pads[1]) expect(copy.pads[1]!.buttons[0]).not.toBe(state.pads[1].buttons[0]);
+ }
+ });
+});
diff --git a/packages/conformance/src/shell/reference-responses.ts b/packages/conformance/src/shell/reference-responses.ts
index a7bb0692..3387670e 100644
--- a/packages/conformance/src/shell/reference-responses.ts
+++ b/packages/conformance/src/shell/reference-responses.ts
@@ -74,6 +74,9 @@ function dataUrlToBlob(url: unknown): { blob: Blob; mime: string } | null {
* envelopes, not the shell's responses.
*/
export const RESPONDERS: Record = {
+ // This transport-neutral mock has no controller API. Production shells use a host reader.
+ 'gamepad.subscribe': () => ok({ type: 'gamepad.state', available: false, reason: 'unavailable', focused: false, pads: [] }),
+ 'gamepad.unsubscribe': none,
// relay
'relay.subscribe': (e) => ok({ type: 'relay.eose', subId: e.subId }),
'relay.close': none,
diff --git a/packages/conformance/src/validators/envelope-specs.ts b/packages/conformance/src/validators/envelope-specs.ts
index 79dac9a5..6abd5759 100644
--- a/packages/conformance/src/validators/envelope-specs.ts
+++ b/packages/conformance/src/validators/envelope-specs.ts
@@ -7,6 +7,11 @@ const ID = { id: 'string' } as const;
* NAP domains, with its direction and (for outbound) required fields.
*/
export const ENVELOPE_SPECS: Record = {
+ // NAP-GAMEPAD Wire Protocol: all three messages are fire-and-forget, without ids.
+ // https://github.com/napplet/naps/pull/108
+ 'gamepad.subscribe': { dir: 'out', forbiddenFields: ['id'], forbiddenReason: 'NAP-GAMEPAD fire-and-forget envelopes do not carry it' },
+ 'gamepad.unsubscribe': { dir: 'out', forbiddenFields: ['id'], forbiddenReason: 'NAP-GAMEPAD fire-and-forget envelopes do not carry it' },
+ 'gamepad.state': { dir: 'in', fields: { available: 'boolean', focused: 'boolean', pads: 'array' } },
// Optional NAP-SHELL environment, independent of other domains.
'shell.ready': { dir: 'out' },
'shell.init': { dir: 'in', fields: { capabilities: 'object', services: 'array' } },
diff --git a/packages/conformance/src/validators/envelope-types.ts b/packages/conformance/src/validators/envelope-types.ts
index 41dbee08..16497c5d 100644
--- a/packages/conformance/src/validators/envelope-types.ts
+++ b/packages/conformance/src/validators/envelope-types.ts
@@ -16,6 +16,8 @@ export interface EnvelopeSpec {
fields?: Record;
/** Fields a napplet-emitted carrier must never supply. */
forbiddenFields?: readonly string[];
+ /** Why `forbiddenFields` are rejected; defaults to "runtime-derived". */
+ forbiddenReason?: string;
}
/** A single problem found while validating an envelope. */
diff --git a/packages/conformance/src/validators/envelope-validation.ts b/packages/conformance/src/validators/envelope-validation.ts
index badea43d..819159d5 100644
--- a/packages/conformance/src/validators/envelope-validation.ts
+++ b/packages/conformance/src/validators/envelope-validation.ts
@@ -210,7 +210,7 @@ export function validateEnvelope(message: unknown): EnvelopeVerdict {
if (field in record) {
errors.push({
code: 'forbidden-field',
- message: `Field "${field}" must be runtime-derived and cannot be emitted by a napplet`,
+ message: `Field "${field}" cannot be emitted by a napplet: ${spec.forbiddenReason ?? 'it must be runtime-derived'}`,
field,
});
}
diff --git a/packages/conformance/src/validators/envelope.test.ts b/packages/conformance/src/validators/envelope.test.ts
index 423c7618..e2de28a4 100644
--- a/packages/conformance/src/validators/envelope.test.ts
+++ b/packages/conformance/src/validators/envelope.test.ts
@@ -73,6 +73,7 @@ describe('validateEnvelope — outbound field checks', () => {
expect(forged.errors).toContainEqual(expect.objectContaining({
code: 'forbidden-field',
field: 'sender',
+ message: expect.stringContaining('runtime-derived'),
}));
});
@@ -237,13 +238,13 @@ describe('validateEnvelope — optional shell domain', () => {
});
describe('ENVELOPE_SPECS invariants', () => {
- it('has 240 discriminants split 115 outbound / 125 inbound', () => {
+ it('has 243 discriminants split 117 outbound / 126 inbound', () => {
const all = knownEnvelopeTypes();
- expect(all).toHaveLength(240);
+ expect(all).toHaveLength(243);
const out = all.filter((t) => ENVELOPE_SPECS[t].dir === 'out');
const inbound = all.filter((t) => ENVELOPE_SPECS[t].dir === 'in');
- expect(out).toHaveLength(115);
- expect(inbound).toHaveLength(125);
+ expect(out).toHaveLength(117);
+ expect(inbound).toHaveLength(126);
});
it('declares canonical inbound INC carrier fields', () => {
diff --git a/packages/conformance/src/validators/gamepad.test.ts b/packages/conformance/src/validators/gamepad.test.ts
new file mode 100644
index 00000000..dabf0f65
--- /dev/null
+++ b/packages/conformance/src/validators/gamepad.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'vitest';
+import { validateEnvelope, knownEnvelopeTypes } from './envelope.js';
+import { createReferenceShell } from '../shell/reference-shell.js';
+
+describe('NAP-GAMEPAD Wire Protocol', () => {
+ it('recognizes exactly the three published messages and no poll/result extension', () => {
+ expect(knownEnvelopeTypes().filter((type) => type.startsWith('gamepad.')).sort()).toEqual(['gamepad.state', 'gamepad.subscribe', 'gamepad.unsubscribe']);
+ expect(validateEnvelope({ type: 'gamepad.getGamepads', id: 'invented' }).ok).toBe(false);
+ expect(validateEnvelope({ type: 'gamepad.subscribe' }).ok).toBe(true);
+ expect(validateEnvelope({ type: 'gamepad.unsubscribe' }).ok).toBe(true);
+ expect(validateEnvelope({ type: 'gamepad.subscribe', id: 'not-defined' }).errors).toContainEqual(expect.objectContaining({ code: 'forbidden-field', field: 'id', message: expect.stringContaining('NAP-GAMEPAD fire-and-forget') }));
+ expect(validateEnvelope({ type: 'gamepad.state', available: true, focused: false, pads: [] }).errors[0].code).toBe('inbound-type-emitted');
+ });
+
+ it('provides an immediate unavailability snapshot from the controller-free mock', () => {
+ const shell = createReferenceShell();
+ expect(shell.handle({ type: 'gamepad.subscribe' })).toEqual([{ type: 'gamepad.state', available: false, reason: 'unavailable', focused: false, pads: [] }]);
+ expect(shell.handle({ type: 'gamepad.unsubscribe' })).toEqual([]);
+ expect(shell.handle({ type: 'gamepad.subscribe', id: 'invalid' })).toEqual([]);
+ });
+});
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index ef97f35f..ece21c44 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,11 @@
# @napplet/core
+## 0.34.0
+
+### Minor Changes
+
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+
## 0.33.0
### Minor Changes
diff --git a/packages/core/README.md b/packages/core/README.md
index 2925635c..46ca96e9 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -57,14 +57,14 @@ interface RelaySubscribe extends NappletMessage {
}
```
-The `type` field domain prefix (`relay`, `identity`, `storage`, `inc`, `theme`, `keys`, `media`, `notify`, `config`, `resource`, `cvm`, `outbox`, `upload`, `intent`, `ble`, `webrtc`, `link`, `count`, `lists`, `serial`, `common`, `dm`) routes messages to the correct NAP handler via `dispatch()`.
+The `type` field domain prefix (`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`) routes messages to the correct NAP handler via `dispatch()`.
#### `NapDomain`
String literal union of the NAP capability domains.
```ts
-type NapDomain = 'relay' | 'identity' | 'storage' | 'inc' | 'theme' | 'keys' | 'media' | 'notify' | 'config' | 'resource' | 'cvm' | 'outbox' | 'upload' | 'intent' | 'ble' | 'webrtc' | 'link' | 'count' | 'lists' | 'serial' | 'common' | 'dm';
+type NapDomain = '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';
```
| Domain | Scope |
@@ -416,3 +416,7 @@ 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).
+
+### GAMEPAD integration
+
+`GamepadState`, `Gamepad`, `GamepadButton`, `GamepadSlot`, `GamepadApi`, and the `gamepad` domain are exported for the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108). Its fire-and-forget envelopes carry no correlation id.
diff --git a/packages/core/jsr.json b/packages/core/jsr.json
index 2fd52efc..be1321ab 100644
--- a/packages/core/jsr.json
+++ b/packages/core/jsr.json
@@ -1,6 +1,6 @@
{
"name": "@napplet/core",
- "version": "0.33.0",
+ "version": "0.34.0",
"license": "MIT",
"exports": "./src/index.ts",
"publish": {
diff --git a/packages/core/package.json b/packages/core/package.json
index ebd2aa7d..d0ff4ca7 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@napplet/core",
- "version": "0.33.0",
+ "version": "0.34.0",
"description": "Shared protocol types, constants, and message definitions for the napplet protocol",
"type": "module",
"main": "./dist/index.js",
diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts
index 5f0a2291..d3b00937 100644
--- a/packages/core/src/envelope.ts
+++ b/packages/core/src/envelope.ts
@@ -69,7 +69,7 @@ export interface NappletMessage {
* const isValid = NAP_DOMAINS.includes(domain); // true
* ```
*/
-export type NapDomain = 'shell' | 'relay' | 'identity' | 'storage' | 'inc' | 'theme' | 'keys' | 'media' | 'notify' | 'config' | 'resource' | 'cvm' | 'outbox' | 'upload' | 'intent' | 'ble' | 'webrtc' | 'link' | 'count' | 'lists' | 'serial' | 'fs' | 'common' | 'dm';
+export type NapDomain = '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';
/**
* Runtime-accessible constant array of all NAP domain names.
@@ -80,4 +80,4 @@ export type NapDomain = 'shell' | 'relay' | 'identity' | 'storage' | 'inc' | 'th
* const selected = NAP_DOMAINS.filter((domain) => domain !== 'ble');
* ```
*/
-export const NAP_DOMAINS: readonly NapDomain[] = ['shell', 'relay', 'identity', 'storage', 'inc', 'theme', 'keys', 'media', 'notify', 'config', 'resource', 'cvm', 'outbox', 'upload', 'intent', 'ble', 'webrtc', 'link', 'count', 'lists', 'serial', 'fs', 'common', 'dm'] as const;
+export const NAP_DOMAINS: readonly NapDomain[] = ['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'] as const;
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index e0dc325c..7b434569 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -237,3 +237,5 @@ export {
} from './boundary.js';
export type { ShellApi, ShellCapabilities, ShellEnvironment } from './types/shell.js';
+
+export type * from './types/gamepad.js';
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 2008c29a..9d48651f 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -36,3 +36,5 @@ export type {
} from './types/global/runtime-api.js';
export type * from './types/shell.js';
+
+export type * from './types/gamepad.js';
diff --git a/packages/core/src/types/gamepad.ts b/packages/core/src/types/gamepad.ts
new file mode 100644
index 00000000..3f3e776e
--- /dev/null
+++ b/packages/core/src/types/gamepad.ts
@@ -0,0 +1,40 @@
+/** Controller snapshots from the living NAP-GAMEPAD proposal: https://github.com/napplet/naps/pull/108. */
+import type { Subscription } from './nostr.js';
+
+/** One controller button, with device-reported press and touch state. */
+export interface GamepadButton {
+ value: number;
+ pressed: boolean;
+ touched: boolean;
+}
+
+/** A connected controller; its index is stable for the connection's lifetime. */
+export interface Gamepad {
+ index: number;
+ id: string;
+ mapping: string;
+ connected: boolean;
+ timestamp: number;
+ axes: number[];
+ buttons: GamepadButton[];
+}
+
+/** An empty slot remains null, preserving the runtime's complete slot list. */
+export type GamepadSlot = Gamepad | null;
+export type GamepadUnavailableReason = 'blocked' | 'unavailable';
+
+/** Complete view of controllers for one napplet, with focus-scoped values. */
+export interface GamepadState {
+ available: boolean;
+ reason?: GamepadUnavailableReason;
+ focused: boolean;
+ pads: GamepadSlot[];
+}
+
+/** The optional, runtime-injected controller domain. Reads never wait for a request. */
+export interface GamepadApi {
+ readonly available: boolean;
+ readonly focused: boolean;
+ getGamepads(): GamepadSlot[];
+ onChange(handler: (state: GamepadState) => void): Subscription;
+}
diff --git a/packages/core/src/types/global.ts b/packages/core/src/types/global.ts
index 4293ae73..d9b97bb7 100644
--- a/packages/core/src/types/global.ts
+++ b/packages/core/src/types/global.ts
@@ -1,3 +1,4 @@
+import type { GamepadApi } from './gamepad.js';
import type { ShellApi } from './shell.js';
import type { RelayApi, IncApi, StorageApi, KeysApi } from './global/nostr-api.js';
@@ -49,6 +50,8 @@ import type {
* domain to the napplet; absence means the domain is unavailable.
*/
export interface NappletGlobal {
+ /** Optional shell-mediated controller input, scoped to this napplet’s focus. */
+ gamepad?: GamepadApi;
/** Optional shell environment; other domains work without it. */
shell?: ShellApi;
/**
diff --git a/packages/nap/CHANGELOG.md b/packages/nap/CHANGELOG.md
index acf73e56..a71360d4 100644
--- a/packages/nap/CHANGELOG.md
+++ b/packages/nap/CHANGELOG.md
@@ -1,5 +1,16 @@
# @napplet/nap
+## 0.34.0
+
+### Minor Changes
+
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+
+### Patch Changes
+
+- Updated dependencies
+ - @napplet/core@0.34.0
+
## 0.33.0
### Minor Changes
diff --git a/packages/nap/README.md b/packages/nap/README.md
index 9ffd1e04..f42dd93e 100644
--- a/packages/nap/README.md
+++ b/packages/nap/README.md
@@ -1,6 +1,6 @@
# @napplet/nap
-> Every active napplet NAP domain (relay, storage, inc, keys, theme, media, notify, identity, config, resource, cvm, outbox, upload, intent, ble, webrtc, link, count, lists, serial, fs, common, dm) as layered subpath exports. The package name remains `@napplet/nap` for compatibility.
+> Every active napplet NAP domain (relay, storage, inc, keys, theme, media, notify, identity, config, resource, cvm, outbox, upload, intent, ble, webrtc, link, count, lists, serial, fs, common, dm, shell, gamepad) as layered subpath exports. The package name remains `@napplet/nap` for compatibility.
## Install
@@ -227,7 +227,7 @@ import { mediaCreateSession } from '@napplet/nap/media/sdk';
import type { MediaNapMessage } from '@napplet/nap/media/types';
```
-Domain barrels are also available at `@napplet/nap/relay`, `@napplet/nap/storage`, `@napplet/nap/inc`, `@napplet/nap/keys`, `@napplet/nap/theme`, `@napplet/nap/media`, `@napplet/nap/notify`, `@napplet/nap/identity`, `@napplet/nap/config`, `@napplet/nap/resource`, `@napplet/nap/cvm`, `@napplet/nap/outbox`, `@napplet/nap/upload`, `@napplet/nap/intent`, `@napplet/nap/ble`, `@napplet/nap/webrtc`, `@napplet/nap/link`, `@napplet/nap/count`, `@napplet/nap/lists`, `@napplet/nap/common`, `@napplet/nap/serial`, `@napplet/nap/fs`, and `@napplet/nap/dm`.
+Domain barrels are also available at `@napplet/nap/relay`, `@napplet/nap/storage`, `@napplet/nap/inc`, `@napplet/nap/keys`, `@napplet/nap/theme`, `@napplet/nap/media`, `@napplet/nap/notify`, `@napplet/nap/identity`, `@napplet/nap/config`, `@napplet/nap/resource`, `@napplet/nap/cvm`, `@napplet/nap/outbox`, `@napplet/nap/upload`, `@napplet/nap/intent`, `@napplet/nap/ble`, `@napplet/nap/webrtc`, `@napplet/nap/link`, `@napplet/nap/count`, `@napplet/nap/lists`, `@napplet/nap/common`, `@napplet/nap/serial`, `@napplet/nap/fs`, `@napplet/nap/dm`, `@napplet/nap/shell`, and `@napplet/nap/gamepad`.
## Optional Peer Dependency
@@ -280,3 +280,23 @@ 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.
diff --git a/packages/nap/jsr.json b/packages/nap/jsr.json
index 6a57e269..6d9219cc 100644
--- a/packages/nap/jsr.json
+++ b/packages/nap/jsr.json
@@ -1,6 +1,6 @@
{
"name": "@napplet/nap",
- "version": "0.33.0",
+ "version": "0.34.0",
"license": "MIT",
"exports": {
"./relay": "./src/relay/index.ts",
@@ -102,10 +102,14 @@
"./shell": "./src/shell/index.ts",
"./shell/types": "./src/shell/types.ts",
"./shell/shim": "./src/shell/shim.ts",
- "./shell/sdk": "./src/shell/sdk.ts"
+ "./shell/sdk": "./src/shell/sdk.ts",
+ "./gamepad": "./src/gamepad/index.ts",
+ "./gamepad/types": "./src/gamepad/types.ts",
+ "./gamepad/shim": "./src/gamepad/shim.ts",
+ "./gamepad/sdk": "./src/gamepad/sdk.ts"
},
"imports": {
- "@napplet/core": "jsr:@napplet/core@^0.33.0",
+ "@napplet/core": "jsr:@napplet/core@^0.34.0",
"nostr-tools/nip19": "npm:nostr-tools@^2.23.3/nip19"
},
"publish": {
diff --git a/packages/nap/package.json b/packages/nap/package.json
index 51511e18..3539bc55 100644
--- a/packages/nap/package.json
+++ b/packages/nap/package.json
@@ -1,7 +1,7 @@
{
"name": "@napplet/nap",
- "version": "0.33.0",
- "description": "The active napplet NAP domains (relay, storage, inc, keys, theme, media, notify, identity, config, resource, cvm, outbox, upload, intent, ble, webrtc, link, count, lists, serial, fs, common, dm, shell) as layered subpath exports",
+ "version": "0.34.0",
+ "description": "The active napplet NAP domains (relay, storage, inc, keys, theme, media, notify, identity, config, resource, cvm, outbox, upload, intent, ble, webrtc, link, count, lists, serial, fs, common, dm, shell, gamepad) as layered subpath exports",
"type": "module",
"exports": {
"./relay": {
@@ -403,6 +403,22 @@
"./shell/sdk": {
"types": "./dist/shell/sdk.d.ts",
"import": "./dist/shell/sdk.js"
+ },
+ "./gamepad": {
+ "types": "./dist/gamepad/index.d.ts",
+ "import": "./dist/gamepad/index.js"
+ },
+ "./gamepad/types": {
+ "types": "./dist/gamepad/types.d.ts",
+ "import": "./dist/gamepad/types.js"
+ },
+ "./gamepad/shim": {
+ "types": "./dist/gamepad/shim.d.ts",
+ "import": "./dist/gamepad/shim.js"
+ },
+ "./gamepad/sdk": {
+ "types": "./dist/gamepad/sdk.d.ts",
+ "import": "./dist/gamepad/sdk.js"
}
},
"files": [
diff --git a/packages/nap/src/gamepad/index.ts b/packages/nap/src/gamepad/index.ts
new file mode 100644
index 00000000..7bfa8476
--- /dev/null
+++ b/packages/nap/src/gamepad/index.ts
@@ -0,0 +1,10 @@
+/** NAP-GAMEPAD package entrypoint. Canonical proposal: https://github.com/napplet/naps/pull/108. */
+import { registerNap } from '@napplet/core';
+import { DOMAIN } from './types.js';
+export { DOMAIN } from './types.js';
+export type * from './types.js';
+export { gamepad, getGamepads, onChange, installGamepadShim, handleGamepadMessage } from './shim.js';
+export { gamepadGetGamepads, gamepadAvailable, gamepadFocused, gamepadOnChange } from './sdk.js';
+export { createGamepadStateTools, isGamepadState, copyGamepadState } from './state.js';
+
+registerNap(DOMAIN, () => { /* The runtime supplies the domain handler. */ });
diff --git a/packages/nap/src/gamepad/sdk.ts b/packages/nap/src/gamepad/sdk.ts
new file mode 100644
index 00000000..004e1a1b
--- /dev/null
+++ b/packages/nap/src/gamepad/sdk.ts
@@ -0,0 +1,37 @@
+/** Application-side GAMEPAD helpers; the runtime injects the domain. */
+import type { GamepadSlot, GamepadState, NappletGlobal, Subscription } from '@napplet/core';
+
+function requireGamepad(): NonNullable {
+ const napplet = (window as Window & { napplet?: NappletGlobal }).napplet;
+ if (!napplet?.gamepad) throw new Error('window.napplet.gamepad is unavailable -- runtime did not inject this domain');
+ return napplet.gamepad;
+}
+
+/**
+ * Read the runtime's latest controller slots without a wire request.
+ * @returns Complete cached slots.
+ * @example const pads = gamepadGetGamepads();
+ */
+export function gamepadGetGamepads(): GamepadSlot[] { return requireGamepad().getGamepads(); }
+
+/**
+ * Read whether the runtime can access controllers.
+ * @returns Cached availability; true before the first state.
+ * @example if (gamepadAvailable()) renderControllers();
+ */
+export function gamepadAvailable(): boolean { return requireGamepad().available; }
+
+/**
+ * Read whether this napplet currently receives live controller values.
+ * @returns Cached input focus; false before the first state.
+ * @example if (gamepadFocused()) updatePlayers();
+ */
+export function gamepadFocused(): boolean { return requireGamepad().focused; }
+
+/**
+ * Observe shell-provided controller snapshots.
+ * @param handler Receives complete focused or neutral snapshots.
+ * @returns Subscription with close() to stop local delivery.
+ * @example const sub = gamepadOnChange((state) => render(state));
+ */
+export function gamepadOnChange(handler: (state: GamepadState) => void): Subscription { return requireGamepad().onChange(handler); }
diff --git a/packages/nap/src/gamepad/shim.test.ts b/packages/nap/src/gamepad/shim.test.ts
new file mode 100644
index 00000000..08c089ab
--- /dev/null
+++ b/packages/nap/src/gamepad/shim.test.ts
@@ -0,0 +1,151 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { GamepadState } from './types.js';
+
+// Connected and neutral examples from NAP-GAMEPAD Wire Protocol, proposal #108.
+const focused: GamepadState = {
+ available: true, focused: true,
+ pads: [null, {
+ index: 1, id: 'Xbox Wireless Controller', mapping: 'standard', connected: true,
+ timestamp: 18235.4, axes: [0, -1, 0, 0],
+ buttons: [{ value: 1, pressed: true, touched: true }, { value: 0, pressed: false, touched: false }],
+ }],
+};
+const neutral: GamepadState = {
+ ...focused, focused: false,
+ pads: [null, { ...focused.pads[1]!, timestamp: 0, axes: [0, 0, 0, 0], buttons: [{ value: 0, pressed: false, touched: false }, { value: 0, pressed: false, touched: false }] }],
+};
+
+let post: ReturnType;
+beforeEach(() => {
+ vi.resetModules();
+ post = vi.fn();
+ vi.stubGlobal('window', { parent: { postMessage: post } });
+});
+afterEach(() => vi.unstubAllGlobals());
+
+describe('NAP-GAMEPAD cached binding', () => {
+ it('starts neutral and subscribes once, on first use, with no correlation id', async () => {
+ const shim = await import('./shim.js');
+ const cleanup = shim.installGamepadShim();
+ const duplicate = shim.installGamepadShim();
+ expect(post).not.toHaveBeenCalled();
+ expect(shim.gamepad.available).toBe(true);
+ expect(shim.gamepad.focused).toBe(false);
+ expect(shim.getGamepads()).toEqual([]);
+ expect(post.mock.calls).toEqual([[{ type: 'gamepad.subscribe' }, '*']]);
+ duplicate();
+ expect(post).toHaveBeenCalledTimes(1);
+ cleanup(); cleanup();
+ expect(post.mock.calls[1]).toEqual([{ type: 'gamepad.unsubscribe' }, '*']);
+ expect(post).toHaveBeenCalledTimes(2);
+ });
+
+ it('preserves empty slots, indexes and complete focused/neutral/blocked states', async () => {
+ const shim = await import('./shim.js');
+ const handler = vi.fn();
+ const cleanup = shim.installGamepadShim();
+ shim.onChange(handler);
+ for (const state of [focused, neutral, { available: false, reason: 'blocked', focused: false, pads: [] }]) {
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...state });
+ expect(handler).toHaveBeenLastCalledWith(state);
+ expect(shim.gamepad.available).toBe(state.available);
+ expect(shim.gamepad.focused).toBe(state.focused);
+ expect(shim.getGamepads()).toEqual(state.pads);
+ }
+ expect(post).toHaveBeenCalledTimes(1);
+ cleanup();
+ });
+
+ it('copies nested input separately for readers and each listener', async () => {
+ const shim = await import('./shim.js');
+ const cleanup = shim.installGamepadShim();
+ shim.onChange((state) => { state.pads[1]!.axes[1] = 0; state.pads[1]!.buttons[0].pressed = false; });
+ const other = vi.fn();
+ shim.onChange(other);
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...focused });
+ const read = shim.getGamepads();
+ read[1]!.axes[1] = 1;
+ expect(other).toHaveBeenCalledWith(focused);
+ expect(shim.getGamepads()).toEqual(focused.pads);
+ cleanup();
+ });
+
+ it('gives repeated handlers independent, idempotent subscription handles', async () => {
+ const shim = await import('./shim.js');
+ const handler = vi.fn();
+ const first = shim.onChange(handler);
+ const second = shim.onChange(handler);
+ first.close(); first.close();
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...focused });
+ expect(handler).toHaveBeenCalledTimes(1);
+ second.close();
+ const third = shim.onChange(handler);
+ first.close();
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...neutral });
+ expect(handler).toHaveBeenCalledTimes(2);
+ third.close();
+ });
+
+ it('sends nothing when installed and cleaned up without use', async () => {
+ const shim = await import('./shim.js');
+ const cleanup = shim.installGamepadShim();
+ cleanup();
+ expect(post).not.toHaveBeenCalled();
+ });
+
+ it('keeps delivering to later listeners when one throws', async () => {
+ const shim = await import('./shim.js');
+ const failure = new Error('listener failed');
+ const reported = vi.fn();
+ vi.stubGlobal('queueMicrotask', reported);
+ shim.onChange(() => { throw failure; });
+ const later = vi.fn();
+ shim.onChange(later);
+ expect(() => shim.handleGamepadMessage({ type: 'gamepad.state', ...focused })).not.toThrow();
+ expect(later).toHaveBeenCalledExactlyOnceWith(focused);
+ expect(reported).toHaveBeenCalledOnce();
+ expect(() => reported.mock.calls[0][0]()).toThrow(failure);
+ });
+
+ it('defers listeners added during delivery until the next snapshot', async () => {
+ const shim = await import('./shim.js');
+ const later = vi.fn();
+ const first = shim.onChange(() => { first.close(); shim.onChange(later); });
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...focused });
+ expect(later).not.toHaveBeenCalled();
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...neutral });
+ expect(later).toHaveBeenCalledExactlyOnceWith(neutral);
+ });
+
+ it('ignores malformed snapshots, request envelopes and state after cleanup', async () => {
+ const shim = await import('./shim.js');
+ const cleanup = shim.installGamepadShim();
+ const handler = vi.fn();
+ shim.onChange(handler);
+ for (const message of [
+ { type: 'gamepad.subscribe' },
+ { type: 'gamepad.state', ...focused, id: 'forbidden' },
+ { type: 'gamepad.state', ...focused, focused: false },
+ { type: 'gamepad.state', available: false, focused: false, pads: [] },
+ { type: 'gamepad.state', ...focused, pads: [false] },
+ ]) shim.handleGamepadMessage(message);
+ expect(handler).not.toHaveBeenCalled();
+ cleanup();
+ shim.handleGamepadMessage({ type: 'gamepad.state', ...focused });
+ expect(handler).not.toHaveBeenCalled();
+ const nextCleanup = shim.installGamepadShim();
+ expect(shim.gamepad.focused).toBe(false);
+ expect(shim.getGamepads()).toEqual([]);
+ nextCleanup();
+ });
+
+ it('checks schema bounds without restricting future mapping strings', async () => {
+ const { isGamepadState } = await import('./state.js');
+ expect(isGamepadState(focused)).toBe(true);
+ expect(isGamepadState(neutral)).toBe(true);
+ expect(isGamepadState({ ...focused, pads: [{ ...focused.pads[1], mapping: 'future-layout' }] })).toBe(true);
+ for (const axis of [-2, 2, NaN, Infinity]) expect(isGamepadState({ ...focused, pads: [{ ...focused.pads[1], axes: [axis] }] })).toBe(false);
+ expect(isGamepadState({ ...focused, reason: 'blocked' })).toBe(false);
+ expect(isGamepadState({ available: false, reason: 'unavailable', focused: false, pads: [] })).toBe(true);
+ });
+});
diff --git a/packages/nap/src/gamepad/shim.ts b/packages/nap/src/gamepad/shim.ts
new file mode 100644
index 00000000..04a7a9e6
--- /dev/null
+++ b/packages/nap/src/gamepad/shim.ts
@@ -0,0 +1,89 @@
+/** Cached NAP-GAMEPAD binding. The runtime owns controller access and focus. */
+import type { GamepadApi, GamepadSlot, GamepadState, Subscription } from '@napplet/core';
+import { postToShell } from '../boundary.js';
+import { copyGamepadState, isGamepadState } from './state.js';
+
+let state: GamepadState = { available: true, focused: false, pads: [] };
+let installed = false;
+let subscribed = false;
+const handlers = new Set<{ handler: (state: GamepadState) => void }>();
+
+function subscribe(): void {
+ if (subscribed) return;
+ subscribed = true;
+ try { postToShell({ type: 'gamepad.subscribe' }); }
+ catch (error) { subscribed = false; throw error; }
+}
+
+/**
+ * Read the latest complete slot list synchronously.
+ * @returns Cached slots; empty before the first snapshot.
+ * @example const pads = getGamepads();
+ */
+export function getGamepads(): GamepadSlot[] {
+ subscribe();
+ return copyGamepadState(state).pads;
+}
+
+/**
+ * Observe each incoming complete state.
+ * @param handler Receives an independent snapshot for each shell update.
+ * @returns An idempotent local subscription handle.
+ * @example const sub = onChange((state) => render(state));
+ */
+export function onChange(handler: (state: GamepadState) => void): Subscription {
+ const entry = { handler };
+ handlers.add(entry);
+ try { subscribe(); }
+ catch (error) { handlers.delete(entry); throw error; }
+ return { close: () => { handlers.delete(entry); } };
+}
+
+/** Optional runtime domain; availability and focus are snapshot-backed getters. */
+export const gamepad: GamepadApi = {
+ get available() { subscribe(); return state.available; },
+ get focused() { subscribe(); return state.focused; },
+ getGamepads,
+ onChange,
+};
+
+/**
+ * Accept shell state through the central parent-source-checked message router.
+ * @param message Incoming envelope.
+ * @returns Nothing; malformed or unrecognized envelopes are ignored.
+ * @example handleGamepadMessage({ type: 'gamepad.state', available: true, focused: false, pads: [] });
+ */
+export function handleGamepadMessage(message: { type: string; [key: string]: unknown }): void {
+ if (!subscribed || message.type !== 'gamepad.state' || 'id' in message || !isGamepadState(message)) return;
+ state = copyGamepadState(message);
+ const snapshot = copyGamepadState(state);
+ const recipients = [...handlers];
+ for (const entry of recipients) {
+ if (!handlers.has(entry)) continue;
+ // A throwing listener must not starve later listeners of state (including neutral snapshots).
+ try { entry.handler(copyGamepadState(snapshot)); }
+ catch (error) { queueMicrotask(() => { throw error; }); }
+ }
+}
+
+/**
+ * Install the domain; the binding subscribes on first use, so unused installs send nothing.
+ * @returns Idempotent cleanup that unsubscribes (if subscribed) and clears the cached state.
+ * @example const cleanup = installGamepadShim();
+ */
+export function installGamepadShim(): () => void {
+ if (installed) return () => undefined;
+ installed = true;
+ let closed = false;
+ return () => {
+ if (closed) return;
+ closed = true;
+ try { if (subscribed) postToShell({ type: 'gamepad.unsubscribe' }); }
+ finally {
+ subscribed = false;
+ installed = false;
+ handlers.clear();
+ state = { available: true, focused: false, pads: [] };
+ }
+ };
+}
diff --git a/packages/nap/src/gamepad/state.ts b/packages/nap/src/gamepad/state.ts
new file mode 100644
index 00000000..cbbecb84
--- /dev/null
+++ b/packages/nap/src/gamepad/state.ts
@@ -0,0 +1,81 @@
+/** Snapshot helpers shared by normal bindings and serialized browser preludes. */
+import type { GamepadState } from './types.js';
+
+/** Pure helpers with no module closures, suitable for pre-execution runtime injection. */
+export interface GamepadStateTools {
+ isGamepadState(value: unknown): value is GamepadState;
+ copyGamepadState(state: GamepadState): GamepadState;
+}
+
+/**
+ * Create self-contained helpers that can be serialized into an opaque iframe.
+ * @returns State validation and independent snapshot copying.
+ * @example const tools = createGamepadStateTools();
+ */
+export function createGamepadStateTools(): GamepadStateTools {
+ function record(value: unknown): value is Record {
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
+ }
+
+ function numberIn(value: unknown, min: number, max: number): value is number {
+ return typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
+ }
+
+ /**
+ * Check the schema and focus-neutrality constraints of a controller snapshot.
+ * @param value Untrusted snapshot payload.
+ * @returns Whether it matches NAP-GAMEPAD Schemas and Shell Behavior.
+ * @example if (isGamepadState(message)) handleGamepadMessage(message);
+ */
+ function isGamepadState(value: unknown): value is GamepadState {
+ if (!record(value) || typeof value.available !== 'boolean' || typeof value.focused !== 'boolean' || !Array.isArray(value.pads)) return false;
+ if (value.available) {
+ if (value.reason !== undefined) return false;
+ } else if (value.focused || (value.reason !== 'blocked' && value.reason !== 'unavailable')) return false;
+ return Array.from(value.pads).every((pad: unknown) => {
+ if (pad === null) return true;
+ if (!record(pad) || !Number.isInteger(pad.index) || (pad.index as number) < 0 || typeof pad.id !== 'string' || typeof pad.mapping !== 'string' || pad.connected !== true) return false;
+ if (typeof pad.timestamp !== 'number' || !Number.isFinite(pad.timestamp) || !Array.isArray(pad.axes) || !Array.isArray(pad.buttons)) return false;
+ if (!value.focused && pad.timestamp !== 0) return false;
+ if (!Array.from(pad.axes).every((axis: unknown) => numberIn(axis, -1, 1) && (value.focused || axis === 0))) return false;
+ return Array.from(pad.buttons).every((button: unknown) => record(button) && numberIn(button.value, 0, 1) && typeof button.pressed === 'boolean' && typeof button.touched === 'boolean' && (value.focused || (button.value === 0 && !button.pressed && !button.touched)));
+ });
+ }
+
+ /**
+ * Copy a snapshot so local readers cannot mutate cached controller values.
+ * @param state Validated complete snapshot.
+ * @returns An independent state and all nested pad/button arrays.
+ * @example const copy = copyGamepadState(state);
+ */
+ function copyGamepadState(state: GamepadState): GamepadState {
+ return {
+ available: state.available,
+ focused: state.focused,
+ ...(state.reason === undefined ? {} : { reason: state.reason }),
+ pads: state.pads.map((pad) => pad === null ? null : {
+ index: pad.index, id: pad.id, mapping: pad.mapping, connected: pad.connected,
+ timestamp: pad.timestamp, axes: [...pad.axes], buttons: pad.buttons.map(({ value, pressed, touched }) => ({ value, pressed, touched })),
+ }),
+ };
+ }
+ return { isGamepadState, copyGamepadState };
+}
+
+const tools = createGamepadStateTools();
+
+/**
+ * Validate a complete snapshot against NAP-GAMEPAD Schemas and Shell Behavior.
+ * @param value Untrusted state payload.
+ * @returns Whether its fields and neutral values conform.
+ * @example if (isGamepadState(message)) accept(message);
+ */
+export function isGamepadState(value: unknown): value is GamepadState { return tools.isGamepadState(value); }
+
+/**
+ * Copy all snapshot data without sharing mutable arrays or buttons.
+ * @param state Validated snapshot.
+ * @returns An independent state.
+ * @example const copy = copyGamepadState(state);
+ */
+export function copyGamepadState(state: GamepadState): GamepadState { return tools.copyGamepadState(state); }
diff --git a/packages/nap/src/gamepad/types.ts b/packages/nap/src/gamepad/types.ts
new file mode 100644
index 00000000..3dcc51a5
--- /dev/null
+++ b/packages/nap/src/gamepad/types.ts
@@ -0,0 +1,17 @@
+/** NAP-GAMEPAD fire-and-forget envelopes; canonical source: https://github.com/napplet/naps/pull/108. */
+import type { GamepadState, NappletMessage } from '@napplet/core';
+
+export const DOMAIN = 'gamepad' as const;
+export type { Gamepad, GamepadApi, GamepadButton, GamepadSlot, GamepadState, GamepadUnavailableReason } from '@napplet/core';
+
+export interface GamepadSubscribeMessage extends NappletMessage {
+ type: 'gamepad.subscribe';
+}
+export interface GamepadUnsubscribeMessage extends NappletMessage {
+ type: 'gamepad.unsubscribe';
+}
+export interface GamepadStateMessage extends NappletMessage, GamepadState {
+ type: 'gamepad.state';
+}
+export type GamepadRequestMessage = GamepadSubscribeMessage | GamepadUnsubscribeMessage;
+export type GamepadNapMessage = GamepadRequestMessage | GamepadStateMessage;
diff --git a/packages/nap/tsup.config.ts b/packages/nap/tsup.config.ts
index 3e70eff3..53269590 100644
--- a/packages/nap/tsup.config.ts
+++ b/packages/nap/tsup.config.ts
@@ -2,6 +2,10 @@ import { defineConfig } from 'tsup';
export default defineConfig({
entry: {
+ 'gamepad/index': 'src/gamepad/index.ts',
+ 'gamepad/types': 'src/gamepad/types.ts',
+ 'gamepad/shim': 'src/gamepad/shim.ts',
+ 'gamepad/sdk': 'src/gamepad/sdk.ts',
'shell/index': 'src/shell/index.ts',
'shell/types': 'src/shell/types.ts',
'shell/shim': 'src/shell/shim.ts',
diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md
index 95334372..a9445ee6 100644
--- a/packages/sdk/CHANGELOG.md
+++ b/packages/sdk/CHANGELOG.md
@@ -1,5 +1,17 @@
# @napplet/sdk
+## 0.30.0
+
+### Minor Changes
+
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+
+### Patch Changes
+
+- Updated dependencies
+ - @napplet/core@0.34.0
+ - @napplet/nap@0.34.0
+
## 0.29.0
### Minor Changes
diff --git a/packages/sdk/README.md b/packages/sdk/README.md
index b55d020e..cc970f8e 100644
--- a/packages/sdk/README.md
+++ b/packages/sdk/README.md
@@ -418,6 +418,7 @@ These are discriminated union types covering all messages in each NAP domain. Us
| `CountNapMessage` | `@napplet/nap/count` | Discriminated union of all count domain messages |
| `ListsNapMessage` | `@napplet/nap/lists` | Discriminated union of all lists domain messages |
| `CommonNapMessage` | `@napplet/nap/common` | Discriminated union of all common domain messages |
+| `GamepadNapMessage` | `@napplet/nap/gamepad` | Subscribe, unsubscribe, and state envelopes without correlation ids |
| `SerialNapMessage` | `@napplet/nap/serial` | Discriminated union of all serial domain messages |
Individual message types (e.g., `RelaySubscribeMessage`, `IdentityGetPublicKeyMessage`) are also re-exported from `@napplet/sdk` for fine-grained typing.
@@ -428,7 +429,7 @@ Each NAP domain has a string constant re-exported from its package:
```ts
import { RELAY_DOMAIN, IDENTITY_DOMAIN, STORAGE_DOMAIN, INC_DOMAIN, THEME_DOMAIN, KEYS_DOMAIN, MEDIA_DOMAIN, NOTIFY_DOMAIN, CONFIG_DOMAIN, RESOURCE_DOMAIN, CVM_DOMAIN, OUTBOX_DOMAIN, UPLOAD_DOMAIN, INTENT_DOMAIN, BLE_DOMAIN, WEBRTC_DOMAIN, LINK_DOMAIN, COUNT_DOMAIN, LISTS_DOMAIN, COMMON_DOMAIN, SERIAL_DOMAIN, DM_DOMAIN } from '@napplet/sdk';
-// Values: 'relay', 'identity', 'storage', 'inc', 'theme', 'keys', 'media', 'notify', 'config', 'resource', 'cvm', 'outbox', 'upload', 'intent', 'ble', 'webrtc', 'link', 'count', 'lists', 'common', 'serial', 'dm'
+// Values: 'relay', 'identity', 'storage', 'inc', 'theme', 'keys', 'media', 'notify', 'config', 'resource', 'cvm', 'outbox', 'upload', 'intent', 'ble', 'webrtc', 'link', 'count', 'lists', 'common', 'serial', 'fs', 'dm', 'shell', 'gamepad'
```
These constants are re-exported from the individual domain packages. Use property presence for type-safe conditional logic:
@@ -506,3 +507,23 @@ 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.
diff --git a/packages/sdk/jsr.json b/packages/sdk/jsr.json
index dd3d5a6c..f425833c 100644
--- a/packages/sdk/jsr.json
+++ b/packages/sdk/jsr.json
@@ -1,11 +1,14 @@
{
"name": "@napplet/sdk",
- "version": "0.29.0",
+ "version": "0.30.0",
"license": "MIT",
- "exports": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts",
+ "./gamepad": "./src/gamepad.ts"
+ },
"imports": {
- "@napplet/core": "jsr:@napplet/core@^0.33.0",
- "@napplet/nap": "jsr:@napplet/nap@^0.33.0"
+ "@napplet/core": "jsr:@napplet/core@^0.34.0",
+ "@napplet/nap": "jsr:@napplet/nap@^0.34.0"
},
"publish": {
"include": [
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 4cb307ed..020744c3 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@napplet/sdk",
- "version": "0.29.0",
+ "version": "0.30.0",
"description": "Typed named exports wrapping window.napplet for bundler-consuming napplet developers",
"type": "module",
"main": "./dist/index.js",
@@ -10,6 +10,10 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
+ },
+ "./gamepad": {
+ "types": "./dist/gamepad.d.ts",
+ "import": "./dist/gamepad.js"
}
},
"files": [
@@ -29,7 +33,8 @@
},
"scripts": {
"build": "tsup",
- "type-check": "tsc --noEmit"
+ "type-check": "tsc --noEmit",
+ "test:unit": "node --test test/*.test.mjs"
},
"license": "MIT",
"repository": {
diff --git a/packages/sdk/src/gamepad.ts b/packages/sdk/src/gamepad.ts
new file mode 100644
index 00000000..b0077e01
--- /dev/null
+++ b/packages/sdk/src/gamepad.ts
@@ -0,0 +1,30 @@
+/** Application-side cached controller input. Guard optional domain presence before use. */
+import type { GamepadApi, GamepadSlot, GamepadState, Subscription } from '@napplet/core';
+import { requireDomain } from './require-napplet.js';
+
+/**
+ * Controller snapshots delivered by the runtime to this napplet.
+ * @example
+ * import { gamepad } from '@napplet/sdk/gamepad';
+ * const pads = gamepad.getGamepads();
+ */
+export const gamepad: GamepadApi = {
+ get available(): boolean { return requireDomain('gamepad').available; },
+ get focused(): boolean { return requireDomain('gamepad').focused; },
+ /**
+ * Read the complete cached slot list synchronously.
+ * @returns Latest controller slots.
+ * @example const pads = gamepad.getGamepads();
+ */
+ getGamepads(): GamepadSlot[] { return requireDomain('gamepad').getGamepads(); },
+ /**
+ * Observe complete snapshots, including neutral focus-loss states.
+ * @param handler Receives each state update.
+ * @returns Local subscription handle.
+ * @example const sub = gamepad.onChange((state) => render(state));
+ */
+ onChange(handler: (state: GamepadState) => void): Subscription { return requireDomain('gamepad').onChange(handler); },
+};
+
+export type { Gamepad, GamepadApi, GamepadButton, GamepadSlot, GamepadState, GamepadUnavailableReason } from '@napplet/core';
+export { gamepadGetGamepads, gamepadAvailable, gamepadFocused, gamepadOnChange } from '@napplet/nap/gamepad/sdk';
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index adfd56d6..3c889895 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -37,3 +37,5 @@ export type * from './nap-types.js';
export * from './nap-runtime.js';
export { shell } from './shell.js';
+
+export { gamepad } from './gamepad.js';
diff --git a/packages/sdk/src/nap-runtime.ts b/packages/sdk/src/nap-runtime.ts
index a853a2f6..faeee5da 100644
--- a/packages/sdk/src/nap-runtime.ts
+++ b/packages/sdk/src/nap-runtime.ts
@@ -205,3 +205,5 @@ export {
} from '@napplet/nap/dm';
export { shellReady, shellSupports, shellOnReady, installShellShim, DOMAIN as SHELL_DOMAIN } from '@napplet/nap/shell';
+
+export { DOMAIN as GAMEPAD_DOMAIN, installGamepadShim, gamepadGetGamepads, gamepadAvailable, gamepadFocused, gamepadOnChange } from '@napplet/nap/gamepad';
diff --git a/packages/sdk/src/nap-types.ts b/packages/sdk/src/nap-types.ts
index f3ae5866..2ef8c9cd 100644
--- a/packages/sdk/src/nap-types.ts
+++ b/packages/sdk/src/nap-types.ts
@@ -9,3 +9,5 @@ export type * from './nap-types-services.js';
export type * from './nap-types-devices.js';
export type { ShellApi, ShellCapabilities, ShellEnvironment } from '@napplet/core';
+
+export type * from '@napplet/nap/gamepad/types';
diff --git a/packages/sdk/test/gamepad.test.mjs b/packages/sdk/test/gamepad.test.mjs
new file mode 100644
index 00000000..2f7dfad2
--- /dev/null
+++ b/packages/sdk/test/gamepad.test.mjs
@@ -0,0 +1,35 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { gamepad } from '@napplet/sdk/gamepad';
+import { gamepad as rootGamepad, GAMEPAD_DOMAIN, gamepadGetGamepads } from '@napplet/sdk';
+
+test('built root and gamepad subpath delegate to the current injected domain', () => {
+ assert.equal(GAMEPAD_DOMAIN, 'gamepad');
+ assert.equal(rootGamepad, gamepad);
+ const previous = globalThis.window;
+ let handler;
+ const pads = [null];
+ const runtime = {
+ available: true, focused: false,
+ getGamepads: () => pads,
+ onChange: (callback) => { handler = callback; return { close: () => { handler = undefined; } }; },
+ };
+ try {
+ globalThis.window = { napplet: { gamepad: runtime } };
+ assert.equal(gamepad.getGamepads(), pads);
+ assert.equal(gamepadGetGamepads(), pads);
+ assert.equal(gamepad.available, true);
+ runtime.focused = true;
+ assert.equal(gamepad.focused, true);
+ const callback = () => {};
+ const sub = gamepad.onChange(callback);
+ assert.equal(handler, callback);
+ sub.close();
+ assert.equal(handler, undefined);
+ globalThis.window.napplet = {};
+ assert.throws(() => gamepad.getGamepads(), /gamepad/);
+ } finally {
+ if (previous === undefined) delete globalThis.window;
+ else globalThis.window = previous;
+ }
+});
diff --git a/packages/sdk/tsup.config.ts b/packages/sdk/tsup.config.ts
index 39620471..3c1cd825 100644
--- a/packages/sdk/tsup.config.ts
+++ b/packages/sdk/tsup.config.ts
@@ -1,7 +1,7 @@
import { defineConfig } from 'tsup';
export default defineConfig({
- entry: ['src/index.ts'],
+ entry: { index: 'src/index.ts', gamepad: 'src/gamepad.ts' },
format: ['esm'],
dts: true,
sourcemap: true,
diff --git a/packages/shim/CHANGELOG.md b/packages/shim/CHANGELOG.md
index 6fd9c56c..42d7dbd2 100644
--- a/packages/shim/CHANGELOG.md
+++ b/packages/shim/CHANGELOG.md
@@ -1,5 +1,17 @@
# @napplet/shim
+## 0.32.0
+
+### Minor Changes
+
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+
+### Patch Changes
+
+- Updated dependencies
+ - @napplet/core@0.34.0
+ - @napplet/nap@0.34.0
+
## 0.31.0
### Minor Changes
diff --git a/packages/shim/README.md b/packages/shim/README.md
index 7a33f562..21ec9ff1 100644
--- a/packages/shim/README.md
+++ b/packages/shim/README.md
@@ -664,3 +664,7 @@ 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).
+
+### GAMEPAD integration
+
+The selected `gamepad` runtime binding installs the cached API, subscribes without SHELL readiness, validates parent state, and unsubscribes during cleanup. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108). The native browser Gamepad API is left untouched pending clarification of the proposal’s optional replacement and NIP-5D’s namespace-only injection rule.
diff --git a/packages/shim/jsr.json b/packages/shim/jsr.json
index aa01ae88..d29855ec 100644
--- a/packages/shim/jsr.json
+++ b/packages/shim/jsr.json
@@ -1,14 +1,14 @@
{
"name": "@napplet/shim",
- "version": "0.31.0",
+ "version": "0.32.0",
"license": "MIT",
"exports": {
".": "./src/index.ts",
"./prelude": "./src/prelude.ts"
},
"imports": {
- "@napplet/core": "jsr:@napplet/core@^0.33.0",
- "@napplet/nap": "jsr:@napplet/nap@^0.33.0"
+ "@napplet/core": "jsr:@napplet/core@^0.34.0",
+ "@napplet/nap": "jsr:@napplet/nap@^0.34.0"
},
"publish": {
"include": [
diff --git a/packages/shim/package.json b/packages/shim/package.json
index 7a0f1603..eded237a 100644
--- a/packages/shim/package.json
+++ b/packages/shim/package.json
@@ -1,7 +1,7 @@
{
"name": "@napplet/shim",
- "version": "0.31.0",
- "description": "Napplet SDK — subscribe, publish, query, emit, on, nappStorage for Nostr-native iframe applications",
+ "version": "0.32.0",
+ "description": "Napplet SDK \u2014 subscribe, publish, query, emit, on, nappStorage for Nostr-native iframe applications",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -34,7 +34,8 @@
"devDependencies": {
"tsup": "^8.5.0",
"typescript": "^5.9.3",
- "vitest": "^4.1.11"
+ "vitest": "^4.1.11",
+ "@napplet/sdk": "workspace:*"
},
"scripts": {
"build": "tsup",
diff --git a/packages/shim/src/gamepad.test.ts b/packages/shim/src/gamepad.test.ts
new file mode 100644
index 00000000..2a00c137
--- /dev/null
+++ b/packages/shim/src/gamepad.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it, vi } from 'vitest';
+import { installNappletRuntimePrelude } from './prelude.js';
+import { gamepad as sdkGamepad } from '@napplet/sdk/gamepad';
+
+function deliver(data: unknown, source: MessageEventSource | null = window.parent): void {
+ window.dispatchEvent(new MessageEvent('message', { data, source }));
+}
+
+describe('runtime-injected GAMEPAD', () => {
+ it('installs a callable optional API, accepts only parent state and backs the SDK getters', () => {
+ const state = { type: 'gamepad.state', available: true, focused: true, pads: [] };
+ const post = vi.spyOn(window.parent, 'postMessage').mockImplementation((message) => {
+ if (message.type === 'gamepad.subscribe') deliver(state);
+ });
+ try {
+ const installed = installNappletRuntimePrelude({ domains: ['gamepad'] });
+ expect(Object.keys(installed)).toEqual(['gamepad']);
+ expect(post).not.toHaveBeenCalled();
+ expect(installed.gamepad!.getGamepads()).toEqual([]);
+ expect(installed.gamepad!.focused).toBe(true);
+ expect(sdkGamepad.focused).toBe(true);
+ expect(post.mock.calls).toEqual([[{ type: 'gamepad.subscribe' }, '*']]);
+ const changed = vi.fn();
+ const sub = sdkGamepad.onChange(changed);
+ const neutral = { ...state, focused: false };
+ deliver(neutral, {} as MessageEventSource);
+ expect(sdkGamepad.focused).toBe(true);
+ expect(changed).not.toHaveBeenCalled();
+ deliver(neutral);
+ expect(sdkGamepad.focused).toBe(false);
+ expect(changed).toHaveBeenCalledExactlyOnceWith({ available: true, focused: false, pads: [] });
+ sub.close();
+ installNappletRuntimePrelude({ domains: [] });
+ expect(() => sdkGamepad.getGamepads()).toThrow('gamepad');
+ deliver(state);
+ expect(changed).toHaveBeenCalledTimes(1);
+ } finally { post.mockRestore(); }
+ });
+});
diff --git a/packages/shim/src/runtime-globals-core.ts b/packages/shim/src/runtime-globals-core.ts
index 1877ea24..37958869 100644
--- a/packages/shim/src/runtime-globals-core.ts
+++ b/packages/shim/src/runtime-globals-core.ts
@@ -1,3 +1,4 @@
+import { gamepad } from '@napplet/nap/gamepad/shim';
import { shell } from '@napplet/nap/shell/shim';
import type { NapDomain, NappletGlobal } from '@napplet/core';
import {
@@ -43,6 +44,7 @@ import {
export function installCoreDomains(domains: ReadonlySet, napplet: Partial): void {
+ if (domains.has('gamepad')) napplet.gamepad = gamepad;
if (domains.has('shell')) napplet.shell = shell;
installFoundationDomains(domains, napplet);
installPresentationDomains(domains, napplet);
diff --git a/packages/shim/src/runtime.ts b/packages/shim/src/runtime.ts
index 5af69c42..a54b7eee 100644
--- a/packages/shim/src/runtime.ts
+++ b/packages/shim/src/runtime.ts
@@ -1,3 +1,4 @@
+import { installGamepadShim, handleGamepadMessage } from '@napplet/nap/gamepad/shim';
import { handleShellMessage, installShellShim } from '@napplet/nap/shell/shim';
import {
NAP_DOMAINS,
@@ -98,6 +99,7 @@ const installedDomainShims = new Set();
let messageListenerInstalled = false;
const DOMAIN_ROUTERS: ReadonlyArray = [
+ ['gamepad.', handleGamepadMessage],
['shell.', handleShellMessage],
['keys.', handleKeysMessage],
['media.', mediaShim.handleMediaMessage],
@@ -148,6 +150,9 @@ function installDomainShim(domain: NapDomain): void {
installedDomainShims.add(domain);
switch (domain) {
+ case 'gamepad':
+ installGamepadShim();
+ return;
case 'shell':
installShellShim();
return;
diff --git a/packages/vite-plugin/CHANGELOG.md b/packages/vite-plugin/CHANGELOG.md
index bdc8bf7d..405d839d 100644
--- a/packages/vite-plugin/CHANGELOG.md
+++ b/packages/vite-plugin/CHANGELOG.md
@@ -1,5 +1,11 @@
# @napplet/vite-plugin
+## 0.16.1
+
+### Patch Changes
+
+- Add the proposed NAP-GAMEPAD cached API, types and subpaths, runtime binding, envelope validation, conformance injection, and optional manifest-domain inference. The coordinated CLI runner follow-up selects these packages after publication. Publish Kehto Paja 0.21.0 before the screenshot package release; conformance CLI 0.3.4 is included in this package set. Native Gamepad API replacement remains deferred pending clarification of the proposal and NIP-5D namespace-only injection restriction.
+
## 0.16.0
### Minor Changes
diff --git a/packages/vite-plugin/README.md b/packages/vite-plugin/README.md
index d9be06f7..631269bb 100644
--- a/packages/vite-plugin/README.md
+++ b/packages/vite-plugin/README.md
@@ -100,3 +100,7 @@ nip5aManifest({
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.
+
+### GAMEPAD integration
+
+Requirement inference recognizes `@napplet/nap/gamepad`, `@napplet/sdk/gamepad`, and direct GAMEPAD domain reads. Declare `optional: ["gamepad"]` when keyboard or pointer controls fulfill the core task. See the proposed [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108).
diff --git a/packages/vite-plugin/jsr.json b/packages/vite-plugin/jsr.json
index e0b09bb4..7f253939 100644
--- a/packages/vite-plugin/jsr.json
+++ b/packages/vite-plugin/jsr.json
@@ -1,11 +1,11 @@
{
"name": "@napplet/vite-plugin",
- "version": "0.16.0",
+ "version": "0.16.1",
"license": "MIT",
"exports": "./src/index.ts",
"imports": {
"parse5": "npm:parse5@^7.3.0",
- "@napplet/nap": "jsr:@napplet/nap@^0.33.0",
+ "@napplet/nap": "jsr:@napplet/nap@^0.34.0",
"nostr-tools": "npm:nostr-tools@^2.23.3",
"vite": "npm:vite@^6.3.0"
},
diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json
index 3cc16b07..abba22fc 100644
--- a/packages/vite-plugin/package.json
+++ b/packages/vite-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@napplet/vite-plugin",
- "version": "0.16.0",
+ "version": "0.16.1",
"description": "Vite plugin for napplet development — generates signed NIP-5A manifests at build time for local testing",
"type": "module",
"main": "./dist/index.js",
diff --git a/packages/vite-plugin/src/index.test.ts b/packages/vite-plugin/src/index.test.ts
index 4b9a8664..551e5a4f 100644
--- a/packages/vite-plugin/src/index.test.ts
+++ b/packages/vite-plugin/src/index.test.ts
@@ -532,6 +532,21 @@ describe('nip5aManifest artifact modes', () => {
]);
});
+ it('recognizes GAMEPAD imports and retains an explicitly optional controller domain', async () => {
+ const fixture = makeFixture();
+ fs.writeFileSync(path.join(fixture.dist, 'index.html'), '');
+ const result = await runCloseBundle(
+ { nappletType: 'controller-fallback', optional: ['gamepad'], requires: { infer: true, explicit: [], mode: 'error' } },
+ fixture,
+ {},
+ [{ id: path.join(fixture.root, 'src/main.ts'), code: "import { gamepad } from '@napplet/sdk/gamepad';\ngamepad.getGamepads();" }],
+ );
+ const tags = readManifest(fixture.dist).tags;
+ expect(tags).toContainEqual(['O', 'gamepad']);
+ expect(tags).not.toContainEqual(['R', 'gamepad']);
+ expect(result.warnings).toEqual([]);
+ });
+
it('does not infer requirements from type-only imports or dynamic window access', async () => {
const fixture = makeFixture();
fs.writeFileSync(path.join(fixture.dist, 'index.html'), '');
diff --git a/packages/vite-plugin/src/requirements.ts b/packages/vite-plugin/src/requirements.ts
index c63e8051..ed0cdb99 100644
--- a/packages/vite-plugin/src/requirements.ts
+++ b/packages/vite-plugin/src/requirements.ts
@@ -32,6 +32,7 @@ const NAP_DOMAINS = [
'fs',
'common',
'dm',
+ 'gamepad',
] as const;
const NAP_DOMAIN_SET = new Set(NAP_DOMAINS);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 526fe875..a80bcb43 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -165,17 +165,17 @@ importers:
packages/conformance-cli:
dependencies:
'@kehto/paja':
- specifier: 0.19.0
- version: 0.19.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))(nostr-tools@2.23.3(typescript@5.9.3))
+ specifier: '>=0.21.0 <0.22.0'
+ version: 0.21.0(@napplet/core@packages+core)(@napplet/nap@packages+nap)(nostr-tools@2.23.3(typescript@5.9.3))
'@napplet/conformance':
specifier: workspace:^
version: link:../conformance
'@napplet/core':
- specifier: 0.32.0
- version: 0.32.0
+ specifier: workspace:*
+ version: link:../core
'@napplet/nap':
- specifier: 0.32.0
- version: 0.32.0(json-schema-to-ts@3.1.1)
+ specifier: workspace:*
+ version: link:../nap
playwright:
specifier: ^1.59.1
version: 1.61.0
@@ -258,6 +258,9 @@ importers:
specifier: workspace:*
version: link:../nap
devDependencies:
+ '@napplet/sdk':
+ specifier: workspace:*
+ version: link:../sdk
tsup:
specifier: ^8.5.0
version: 8.5.1(jiti@2.6.1)(postcss@8.5.23)(typescript@5.9.3)(yaml@2.9.1)
@@ -683,50 +686,50 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
- '@kehto/acl@0.19.0':
- resolution: {integrity: sha512-A2wxhWOLWbN3wiCofFs+WKZ5NcHXgbJvXucwJK51Z03Qh705JQ0T/cW1FYVYrJZTEvMyVN0QEGfybOwMLln/Kw==}
+ '@kehto/acl@0.19.1':
+ resolution: {integrity: sha512-4Ib3Gql2AtR1mmjyaLm0VATLIQkbZvCXV31NcaD6jAWZoeQy0dHg+6iSDu8XPGroIJdBMbPUDfGFGwVMi17Gmw==}
peerDependencies:
- '@napplet/core': '>=0.32.0 <0.33.0'
- '@napplet/nap': '>=0.32.0 <0.33.0'
+ '@napplet/core': '>=0.32.0 <0.35.0'
+ '@napplet/nap': '>=0.32.0 <0.35.0'
- '@kehto/firewall@0.6.0':
- resolution: {integrity: sha512-KRWlh2NJrT/7L1LbXnL6IY+ASgrqivuoOwBn892xDuVHoOfBZrafH7bWxcwsLahDxu4iQmXRhEa/ToiaMDhyaQ==}
+ '@kehto/firewall@0.6.1':
+ resolution: {integrity: sha512-TlrwRjVlmEWdx8CpUeaMjlVoVQ7N2cGLxYKyK/b03eoUNzUjK8xvwMuWrbRH2gJJLP0c8k51YipWF1tedpsNfA==}
peerDependencies:
- '@napplet/core': '>=0.32.0 <0.33.0'
+ '@napplet/core': '>=0.32.0 <0.35.0'
- '@kehto/nip@0.6.0':
- resolution: {integrity: sha512-RhiLX0NYbOZ+rxq+wbWrcZiHE7F7nasaDCXzpk8TTxDsJvDdLjUISe6o5udMwry5sTya9S40F91D7m0dYd2Gqg==}
+ '@kehto/nip@0.7.0':
+ resolution: {integrity: sha512-uDi2fvs8gr2jhhNKU8JbrDaq1cqRpAHdxDWBpoLv59tVTTFcTul/bWpT0aAHcq/SzHgT+3IHT0oIuqJXpqCwhw==}
peerDependencies:
nostr-tools: '>=2.23.3 <=2.x'
- '@kehto/paja@0.19.0':
- resolution: {integrity: sha512-UCUwER9thz/sP3B1BPhM/QkymesjnFKGfeNLtuIX0lT1xPH+q8Tbl149U7P2OdgAWt+p5n8p/T/WIOXTFE1CBQ==}
+ '@kehto/paja@0.21.0':
+ resolution: {integrity: sha512-Tk5X3XKVjXzIEhXJWtxpmlNNwCv+jcmfWyuZIby3V/dm+uB4eHjwhYijhzoZvUaHY1DmawCI3wRvbKjCVQyydw==}
peerDependencies:
- '@napplet/core': '>=0.32.0 <0.33.0'
- '@napplet/nap': '>=0.32.0 <0.33.0'
+ '@napplet/core': '>=0.32.0 <0.35.0'
+ '@napplet/nap': '>=0.32.0 <0.35.0'
nostr-tools: '>=2.23.3 <=2.x'
- '@kehto/runtime@0.24.0':
- resolution: {integrity: sha512-uWjopIDhFTbduBZ1iPmm6dKZyV/4nySb52ODBeqeWo9ErVIbYt0v3j/RGgeYvGNRJbCxqqN/Rllpec1FWFgoqA==}
+ '@kehto/runtime@0.24.1':
+ resolution: {integrity: sha512-cEaGbhVlVss6xZJAiGFQAssFaG5PXtPS/kZL/EjsgtT7KX6chybXialLwAj9HUGrIADUO2S2SMY36zvIzmH5Ew==}
peerDependencies:
- '@napplet/core': '>=0.32.0 <0.33.0'
- '@napplet/nap': '>=0.32.0 <0.33.0'
+ '@napplet/core': '>=0.32.0 <0.35.0'
+ '@napplet/nap': '>=0.32.0 <0.35.0'
- '@kehto/services@0.22.1':
- resolution: {integrity: sha512-7rn8IGhqJmN922eO++UmcB9J1rMhqkDUMMne7zSX6Rd1krn2a98Z17req/rD/Ed67ByrMMjkbD11OU+avdRuig==}
+ '@kehto/services@0.24.0':
+ resolution: {integrity: sha512-hky7aZjK13+fZqyKnNIiv8sfkpEc8Xx+sQivJ3qOrXz3Jw3WCczTcSK1+XpPsCHIBbFKXXOzvktQJsCh67N4xg==}
peerDependencies:
- '@napplet/core': '>=0.32.0 <0.33.0'
- '@napplet/nap': '>=0.32.0 <0.33.0'
+ '@napplet/core': '>=0.32.0 <0.35.0'
+ '@napplet/nap': '>=0.32.0 <0.35.0'
nostr-tools: '>=2.23.3 <=2.x'
peerDependenciesMeta:
nostr-tools:
optional: true
- '@kehto/shell@0.22.0':
- resolution: {integrity: sha512-YFPxZdFiZ73u40TKU5EOjnsH2P2pRfni4NWppyGwcBYUSkzsiNZs5G63ay3D/Ugr5hLznnNcu5HB88K6QbJ0HQ==}
+ '@kehto/shell@0.24.0':
+ resolution: {integrity: sha512-dfi/REfv7bWoCyTutZeLC3ZKIuVs7VxA84FHFXJYcHGouwecTzneBPmHnM+lgFCq7wi8tuPKIdVS43uxMNsR/Q==}
peerDependencies:
- '@napplet/core': '>=0.32.0 <0.33.0'
- '@napplet/nap': '>=0.32.0 <0.33.0'
+ '@napplet/core': '>=0.32.0 <0.35.0'
+ '@napplet/nap': '>=0.32.0 <0.35.0'
nostr-tools: '>=2.23.3 <=2.x'
'@kehto/wm@0.0.2':
@@ -744,17 +747,6 @@ packages:
resolution: {integrity: sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==}
engines: {node: '>=20.0.0'}
- '@napplet/core@0.32.0':
- resolution: {integrity: sha512-gP+U7xd32gD7uFYINK+NWDJfW2TMvhLcIPr+Boafta7tZWTRIlyzozvqsooyFg7XUapCOnGKqnjIUa0DFo6Brw==}
-
- '@napplet/nap@0.32.0':
- resolution: {integrity: sha512-DvlneleMpcUtOZmLGRGj9QSCor67veL4gWOYGTWaXPDkBEaEuiU4Ijtreb3+kVky/4ghjL3qmScOFMW6WorWQw==}
- peerDependencies:
- json-schema-to-ts: ^3.1.1
- peerDependenciesMeta:
- json-schema-to-ts:
- optional: true
-
'@noble/ciphers@0.5.3':
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
@@ -2480,56 +2472,56 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@kehto/acl@0.19.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))':
+ '@kehto/acl@0.19.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)':
dependencies:
- '@napplet/core': 0.32.0
- '@napplet/nap': 0.32.0(json-schema-to-ts@3.1.1)
+ '@napplet/core': link:packages/core
+ '@napplet/nap': link:packages/nap
- '@kehto/firewall@0.6.0(@napplet/core@0.32.0)':
+ '@kehto/firewall@0.6.1(@napplet/core@packages+core)':
dependencies:
- '@napplet/core': 0.32.0
+ '@napplet/core': link:packages/core
- '@kehto/nip@0.6.0(nostr-tools@2.23.3(typescript@5.9.3))':
+ '@kehto/nip@0.7.0(nostr-tools@2.23.3(typescript@5.9.3))':
dependencies:
'@noble/hashes': 2.0.1
nostr-tools: 2.23.3(typescript@5.9.3)
- '@kehto/paja@0.19.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))(nostr-tools@2.23.3(typescript@5.9.3))':
+ '@kehto/paja@0.21.0(@napplet/core@packages+core)(@napplet/nap@packages+nap)(nostr-tools@2.23.3(typescript@5.9.3))':
dependencies:
- '@kehto/acl': 0.19.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))
- '@kehto/firewall': 0.6.0(@napplet/core@0.32.0)
- '@kehto/nip': 0.6.0(nostr-tools@2.23.3(typescript@5.9.3))
- '@kehto/runtime': 0.24.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))
- '@kehto/services': 0.22.1(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))(nostr-tools@2.23.3(typescript@5.9.3))
- '@kehto/shell': 0.22.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))(nostr-tools@2.23.3(typescript@5.9.3))
+ '@kehto/acl': 0.19.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)
+ '@kehto/firewall': 0.6.1(@napplet/core@packages+core)
+ '@kehto/nip': 0.7.0(nostr-tools@2.23.3(typescript@5.9.3))
+ '@kehto/runtime': 0.24.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)
+ '@kehto/services': 0.24.0(@napplet/core@packages+core)(@napplet/nap@packages+nap)(nostr-tools@2.23.3(typescript@5.9.3))
+ '@kehto/shell': 0.24.0(@napplet/core@packages+core)(@napplet/nap@packages+nap)(nostr-tools@2.23.3(typescript@5.9.3))
'@kehto/wm': 0.0.2
- '@napplet/core': 0.32.0
- '@napplet/nap': 0.32.0(json-schema-to-ts@3.1.1)
+ '@napplet/core': link:packages/core
+ '@napplet/nap': link:packages/nap
nostr-tools: 2.23.3(typescript@5.9.3)
- '@kehto/runtime@0.24.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))':
+ '@kehto/runtime@0.24.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)':
dependencies:
- '@kehto/acl': 0.19.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))
- '@kehto/firewall': 0.6.0(@napplet/core@0.32.0)
- '@napplet/core': 0.32.0
- '@napplet/nap': 0.32.0(json-schema-to-ts@3.1.1)
+ '@kehto/acl': 0.19.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)
+ '@kehto/firewall': 0.6.1(@napplet/core@packages+core)
+ '@napplet/core': link:packages/core
+ '@napplet/nap': link:packages/nap
'@noble/curves': 2.0.1
'@noble/hashes': 2.0.1
- '@kehto/services@0.22.1(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))(nostr-tools@2.23.3(typescript@5.9.3))':
+ '@kehto/services@0.24.0(@napplet/core@packages+core)(@napplet/nap@packages+nap)(nostr-tools@2.23.3(typescript@5.9.3))':
dependencies:
- '@kehto/runtime': 0.24.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))
- '@napplet/core': 0.32.0
- '@napplet/nap': 0.32.0(json-schema-to-ts@3.1.1)
+ '@kehto/runtime': 0.24.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)
+ '@napplet/core': link:packages/core
+ '@napplet/nap': link:packages/nap
optionalDependencies:
nostr-tools: 2.23.3(typescript@5.9.3)
- '@kehto/shell@0.22.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))(nostr-tools@2.23.3(typescript@5.9.3))':
+ '@kehto/shell@0.24.0(@napplet/core@packages+core)(@napplet/nap@packages+nap)(nostr-tools@2.23.3(typescript@5.9.3))':
dependencies:
- '@kehto/acl': 0.19.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))
- '@kehto/runtime': 0.24.0(@napplet/core@0.32.0)(@napplet/nap@0.32.0(json-schema-to-ts@3.1.1))
- '@napplet/core': 0.32.0
- '@napplet/nap': 0.32.0(json-schema-to-ts@3.1.1)
+ '@kehto/acl': 0.19.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)
+ '@kehto/runtime': 0.24.1(@napplet/core@packages+core)(@napplet/nap@packages+nap)
+ '@napplet/core': link:packages/core
+ '@napplet/nap': link:packages/nap
nostr-tools: 2.23.3(typescript@5.9.3)
'@kehto/wm@0.0.2': {}
@@ -2549,14 +2541,6 @@ snapshots:
tinyglobby: 0.2.17
yaml: 2.9.1
- '@napplet/core@0.32.0': {}
-
- '@napplet/nap@0.32.0(json-schema-to-ts@3.1.1)':
- dependencies:
- '@napplet/core': 0.32.0
- optionalDependencies:
- json-schema-to-ts: 3.1.1
-
'@noble/ciphers@0.5.3': {}
'@noble/ciphers@2.1.1': {}
diff --git a/skills/napplet-build/SKILL.md b/skills/napplet-build/SKILL.md
index 0b2b9389..6c790069 100644
--- a/skills/napplet-build/SKILL.md
+++ b/skills/napplet-build/SKILL.md
@@ -100,3 +100,7 @@ Report the URL Paja prints. A raw Vite URL is an asset server without a runtime
- `localStorage`, `fetch`, `
`, `WebSocket`, `window.nostr` — none exist in the sandbox.
- External `