feat(proxy): /entitlement endpoint + tier-aware /pair-client cap (PR 2/5 — premium tier)#49
Draft
DocNR wants to merge 1 commit into
Draft
feat(proxy): /entitlement endpoint + tier-aware /pair-client cap (PR 2/5 — premium tier)#49DocNR wants to merge 1 commit into
DocNR wants to merge 1 commit into
Conversation
PR 2 of the premium-tier series. Wires the entitlements module from PR #48 (`feat/entitlements-storage`) into proxy.js — proxy is now the authoritative source for tier resolution and cap enforcement. Changes: 1. Init entitlementsStorage alongside clientsStorage + tokensStorage, reading from `./entitlements.json` (created on first `setEntitlement` call; missing file = everyone is free, no behavior change for current users). 2. New `GET /entitlement?pubkey=<hex>` endpoint: - NIP-98 auth required (any signer — entitlement state is roughly public; auth keeps scrapers out) - Returns {pubkey, tier, max_accounts, max_clients, granted_at?, expires_at?, granted_by?} - Optional `X-APNs-Token-Prefix: <8 hex>` header records the querying device into `devices_seen` for the abuse tripwire, ONLY if the queried pubkey has a non-free entitlement (free tier doesn't accumulate device data) - Wrapped in async IIFE because the outer http.createServer callback is sync (same pattern other endpoints use for POST bodies) 3. `/pair-client` cap-check is now tier-aware: - Replaced hardcoded `> 5` with `> entitlementsStorage.maxClientsForTier(tier)` - Default tier "free" → cap 5 (no behavior change for unprivileged users); tier "premium" → cap 30 - Error code stays `pairing_limit` for iOS backwards compat; response gains `tier` field + `limit` carries the dynamic cap iOS will start consuming /entitlement in PR 4 (model + service); the tier-aware cap takes effect server-side regardless of iOS version, but without granted entries everyone stays at the existing 5-client cap. Tests: full proxy suite green at 111/111. Endpoint integration tests require an HTTP test harness which doesn't exist in this codebase yet — verification will happen via test-proxy smoke tests + iOS device tests in PR 4/5. Stacks on PR #48 (entitlements storage module). Will rebase if PR #48 receives review changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
Author
|
Paused — queued for Phase 2. This is part of a server-side entitlement series (#48 → #49 → #50, plus an uncommitted iOS service layer on Preserved as draft because:
Architecture decision (Apple IAP vs Lightning device-bound vs Lightning npub-bound) deferred until there's a real monetization timeline. No action needed here. Will revisit when payment work begins. Plan: |
This was referenced May 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR 2 of the premium-tier series. Wires the entitlements module from PR #48 into
proxy.js— the proxy is now the authoritative source for tier resolution and cap enforcement.Stacks on #48. Base branch is
feat/entitlements-storage. Will rebase if PR #48 receives review changes. Merge order: PR #48 → PR 2.What this changes
1.
entitlementsStorageinitialized at bootAlongside
clientsStorageandtokensStorage. Reads from./entitlements.json. Missing file = everyone is free → no behavior change for current users.2. New endpoint:
GET /entitlement?pubkey=<hex>{ "pubkey": "<hex>", "tier": "free" | "premium", "max_accounts": 4 | 10, "max_clients": 5 | 30, "granted_at": 1714000000, // present if entry exists "expires_at": null, // present if entry exists "granted_by": "admin:..." // present if entry has source }X-APNs-Token-Prefix: <8 hex>: records the querying device intodevices_seenfor the abuse tripwire, ONLY if the queried pubkey has a non-free entitlement (free tier doesn't accumulate device data — privacy-preserving)Wrapped in async IIFE because the outer
http.createServercallback is sync; same pattern other endpoints use viareq.on("end", async () => …)for POST bodies.3. Tier-aware cap in
/pair-clientif (projectedCount > 5)withif (projectedCount > entitlementsStorage.maxClientsForTier(tier))"free"→ cap 5 (no behavior change for unprivileged users); tier"premium"→ cap 30pairing_limitfor iOS backwards compattierfield;limitcarries the dynamic capVerification
Unit tests
node --check proxy.js— syntax OKSmoke tests on
proxy-test.clave.casa(refreshed to current main, then this branch deployed)Service logs:
What's NOT verified yet (intentional — PR 4/5 covers these)
nakintegration)The unit-test coverage of the underlying entitlements module (32 tests in PR #48) covers the read paths this endpoint exercises.
Files changed
relay-proxy/proxy.js— 99 insertions, 2 deletions.Risk
Low. Production proxy is unaffected (this PR ships to
proxy-test.clave.casafor verification; deploy toproxy.clave.casaonly after PR #48 + this PR merge). Even on the test proxy, behavior is unchanged for existing users (no granted entitlements yet → everyone free → cap stays 5).🤖 Generated with Claude Code