Caveat 2 (premium sub had no data source) -> RESOLVED:
- src/services/subscriptionService.js: resolves a premium tier by npub. Two
sources, both optional (absent => null => unchanged): PREMIUM_NPUBS operator
allowlist (npub1/hex[:tier]) works TODAY with zero backend dependency, and an
optional SMIRK_PREMIUM_STATUS_URL HTTP endpoint for full automation. Cached.
- identityService.entitlementsForUser populates user.subscription_tier from it
(by smirk_public_key) before the snapshot, so an active sub unlocks cosmetics.
(The Smirk backend's /premium/status is self-only, so a by-npub service lookup
is the one remaining backend addition for full automation — documented.)
Caveat 1 (client signing was bare scaffold) -> HARDENED:
- html/js/smirkChatSign.js now does the Nostr-scope-grant retry (sign first; on
NOT_AUTHORIZED, getNostrPublicKey() once + retry), same as SmirkAuth — so a
returning user signs with one approval instead of silently falling back. Still
feature-detected + graceful. A real Smirk wallet click is the last confirm.
Docs updated (MONETIZATION.md, CHAT_AND_NOSTR.md). Tests: +6 (allowlist hex/npub1,
HTTP active/inactive+cache, identityService unlock, non-premium unchanged).
Suite 57 suites / 413 tests green.
Phase 2 — a signed-in player can post global chat under their OWN Smirk npub
instead of the shared bridge account:
- src/utils/verifyChatEvent.js: verify a client-signed chat event with the same
defenses as verifyNip98Event (rebuild clean event, recompute id, verify schnorr,
freshness) PLUS the impersonation guard — the event's pubkey MUST equal the
session's authenticated npub (users.smirk_public_key from NIP-98 login).
- ChatProvider.relaySignedEvent (base: local delivery only) + NostrChatProvider
override (local delivery + publish the PRE-SIGNED event to the relays, no
re-signing, echo-deduped) so it carries the player's npub.
- chatHandler.handleSignedChatMessage: rate-limit + resolve the session npub +
verify + escape + relay. socketHandlers wires the event. Serves
lobby + tavern (both render the global broadcast).
- html/js/smirkChatSign.js + tavern send wire: feature-detected client helper —
signs via window.smirk.signNostrEvent, falls back to the unsigned send when
Smirk isn't connected, so it's inert (and safe) until a page has a wallet.
Premium -> cosmetics (the P1 catalog already had the hook):
- entitlements.levelForUser now takes the HIGHER of premium_level and an
active-subscription tier, so a wowne.ro premium sub sets the entitlement tier
and unlocks tile/customization packs at/below it — one sub, chat perks +
cosmetics. Buying credits still never joins the premium ladder.
Behavior-preserving: no subscription_tier => unchanged; chat_signed only fires
when a client opts in. The client signing path needs browser verification against
a connected Smirk wallet (like the NIP-98 login). Tests: +13 (verify incl.
impersonation/tamper/channel/freshness, relay+echo-dedupe, sub-tier unlock).
Suite 56 suites / 407 tests green.