@nibgate/sdk 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -259,16 +259,88 @@ The package includes small controller UIs, not a heavy design system:
259
259
 
260
260
  Creators keep their own UI/theme. Nibgate provides the hard parts: resource normalization, metadata validation, x402/Gateway unlock, transfer fallback, event streaming, proof storage, rating tx submission, and hub sync.
261
261
 
262
+ ## Security
263
+
264
+ ### Premium content must NEVER be in the HTML
265
+
266
+ This is the most important rule. Paid content (body, media URLs, download links) **must not exist in the initial page HTML**. Including it with `display: none` or `hidden` is not secure — anyone can view the page source or use DevTools to see it.
267
+
268
+ **Correct architecture:**
269
+
270
+ ```
271
+ [Browser] [Your Server] [Hub] [Circle]
272
+ │ │ │ │
273
+ ├── GET /api/content/:id ────┤ │ │
274
+ │ ├── returns teaser ─────┤ │
275
+ │ (NO body in HTML) │ (no body for paid) │ │
276
+ │ │ │ │
277
+ ├── GET /api/nibgate/access ─┤ │ │
278
+ │ with stored proof ├── verifyProof() ──────┤ │
279
+ │◄─── 200 { content } ───────┤ │ │
280
+ │ │ │ │
281
+ │ [Content rendered from │ │ │
282
+ │ server response, never │ │ │
283
+ │ in page source] │ │ │
284
+ ```
285
+
286
+ **Implementation pattern (backend):**
287
+
288
+ ```js
289
+ // Public endpoint — NEVER returns body for paid posts
290
+ router.get('/posts/:slug', async (req, res) => {
291
+ const post = await db.post.findUnique({ where: { slug: req.params.slug } });
292
+ if (post.price) {
293
+ const { body, ...teaser } = post; // strip body
294
+ return res.json({ post: { ...teaser, isLocked: true } });
295
+ }
296
+ res.json({ post });
297
+ });
298
+
299
+ // Protected endpoint — returns body only after proof verification
300
+ router.get('/nibgate/access', async (req, res) => {
301
+ const access = nibgateServer.accessFor(request, resource);
302
+ if (access.allowed) {
303
+ const post = await db.post.findUnique({ where: { slug } });
304
+ return res.json({ ok: true, content: post.body }); // body ONLY here
305
+ }
306
+ // ... 402 challenge or payment processing
307
+ });
308
+ ```
309
+
310
+ **Implementation pattern (frontend):**
311
+
312
+ ```js
313
+ // The NibgateUnlock component handles everything:
314
+ // 1. On mount: sends stored proof → gets content immediately if valid
315
+ // 2. If 402: shows unlock button → MetaMask signs → sends proof → gets content
316
+ // 3. Content is NEVER in the HTML, only from server response
317
+
318
+ function NibgateUnlock({ resource }) {
319
+ const [content, setContent] = useState('');
320
+
321
+ useEffect(() => {
322
+ fetch('/api/nibgate/access', {
323
+ headers: { 'x-nibgate-payment-proof': storedProof }
324
+ }).then(res => res.json()).then(data => {
325
+ if (data.content) setContent(data.content);
326
+ });
327
+ }, []);
328
+
329
+ if (content) return <div>{content}</div>;
330
+ return <button onClick={handleUnlock}>Unlock</button>;
331
+ }
332
+ ```
333
+
262
334
  ## FAQ / integration gotchas
263
335
 
264
- - `Does a creator need a server?` Yes for real paid gating. Static-only sites can register discovery events, but protected content needs a server, edge function, CMS webhook, or API route that can return `402` and verify payment proof.
336
+ - `Does a creator need a server?` Yes for real paid gating. Static-only sites can register discovery events, but protected content needs a server, edge function, CMS webhook, or API route that can return `402` and verify payment proof. The premium body must come from a protected route, not from client-side hiding.
265
337
  - `Can every post pay a different wallet?` Yes. Set `recipient` or `payTo` per resource. The package does not force one site-wide receiver.
266
338
  - `Can this work with DB blogs, MDX, CMS, plain HTML, or custom apps?` Yes. Convert each content item into a Nibgate resource with `id`, `title`, `type`, `url`, `path`, `price`, and `recipient`.
267
339
  - `Why does content not show in Explore?` Usually the widget is missing, the site is not verified, the manifest has not synced yet, `url` is not absolute, or required metadata is missing.
268
340
  - `Why does reputation not update?` The rating wallet must have an unlock/payment proof for that content, the rating tx must be onchain, and the backend indexer must sync the tx.
269
341
  - `Are page views reputation?` No. Views, referrers, scroll depth, and time spent are analytics signals. Reputation-critical scores come from indexed onchain ratings tied to unlock proof.
270
342
  - `Do agents use a different flow?` No. Agents discover the same resource metadata, pay through the same protected route, and can emit/index reputation using the same proof model.
271
- - `Should hidden content live in browser HTML?` No. The browser should only receive teasers before payment. The full body/media URL should come from a protected route after proof verification.
343
+ - `Should hidden content live in browser HTML?` **Never.** The browser should only receive teasers before payment. The full body/media URL should come from a protected route after proof verification. See the Security section above.
272
344
  - `What breaks most often locally?` Missing wallet provider, wrong chain, missing Gateway client module, CORS on the backend, wrong `NIBGATE_API_BASE`, wrong site token, or a demo route using fallback challenge mode instead of real Gateway mode.
273
345
 
274
346
 
package/SKILL.md CHANGED
@@ -53,6 +53,9 @@ If the widget loads after app code, the SDK queues events and flushes them when
53
53
  <button data-nibgate-unlock-btn>Unlock for 0.01 USDC</button>
54
54
  <p data-nibgate-status></p>
55
55
  </div>
56
+ <!-- WARNING: This hidden pattern is NOT secure. Content is in the HTML.
57
+ Use only for teasers or non-sensitive content.
58
+ For real paid gating, serve content from a protected server endpoint. -->
56
59
  <div data-nibgate-unlocked hidden>
57
60
  Full premium content here...
58
61
  </div>
@@ -61,6 +64,8 @@ If the widget loads after app code, the SDK queues events and flushes them when
61
64
 
62
65
  No SDK import, no server access route, no env vars. The default wallet is set once in the Nibgate dashboard — or override per-post with `data-nibgate-recipient`. Custom price per-post: change `data-nibgate-premium="0.05"`.
63
66
 
67
+ > **⚠️ Security warning:** Hosted mode puts premium content in the page HTML (hidden by CSS). Anyone can view-source and read it. For real paid content, use server-side gating with the `accessResponse` or `protect` methods. See the Security section below.
68
+
64
69
  ---
65
70
 
66
71
  ## 2. Resource Shape (Minimal)
@@ -311,11 +316,14 @@ For v1, treat `unlock.mode: 'one_time'` with Circle Gateway/x402 as the producti
311
316
  ### How payment works
312
317
 
313
318
  1. Browser requests access → server returns 402 challenge with creator's wallet, price, network
314
- 2. Browser prompts user to sign a Gateway payment payload (EIP-712 typed data via `eth_signTypedData_v4`)
319
+ 2. Browser prompts user to sign a Gateway payment payload (EIP-712 typed data via viem's `walletClient.signTypedData()` through MetaMask)
315
320
  3. Signed payload sent to server as `payment-signature` header
316
321
  4. Server verifies with `createGatewayMiddleware` from `@circle-fin/x402-batching/server`
317
- 5. On success, server creates an HMAC-signed unlock token and returns it
322
+ 5. On success, server creates an HMAC-signed unlock token and returns it directly with the **premium content body**
318
323
  6. Browser stores the token as `x-nibgate-payment-proof` for subsequent requests
324
+ 7. On return visits, browser sends stored proof → server verifies → returns content without payment
325
+
326
+ > **⚠️ Important:** Use viem's `createWalletClient` + `custom(window.ethereum)` transport instead of raw `eth_signTypedData_v4`. This ensures consistent EIP-712 hashing across wallets (MetaMask, Rabby, etc.). Raw `eth_signTypedData_v4` can produce hashes that don't match what the server expects.
319
327
 
320
328
  ### Before testing locally
321
329
 
@@ -630,7 +638,89 @@ Events emitted automatically by `createNibgateServer`: `payment_completed`, `unl
630
638
 
631
639
  ---
632
640
 
633
- ## 16. Framework Notes
641
+ ## 16. Security: Content Must Never Be in HTML
642
+
643
+ This is the single most important rule for paid content:
644
+
645
+ **❌ Wrong (insecure):**
646
+ ```html
647
+ <div data-nibgate-premium>
648
+ <div data-nibgate-unlocked hidden>
649
+ <!-- Premium content visible in page source -->
650
+ </div>
651
+ </div>
652
+ ```
653
+
654
+ **✅ Correct (secure):**
655
+ ```
656
+ 1. Backend: Public endpoint returns post WITHOUT body for paid content
657
+ 2. Frontend: Shows unlock button (no content in HTML)
658
+ 3. After payment: Backend returns content ONLY via the protected access endpoint
659
+ 4. Frontend: Renders content from the server response (never in source)
660
+ ```
661
+
662
+ ### Architecture
663
+
664
+ ```
665
+ [Public endpoint] [Protected endpoint]
666
+ GET /api/posts/:slug GET /api/nibgate/access
667
+ ├── free post → return body ├── Has valid proof → return body + unlock token
668
+ └── paid post → return teaser └── No proof → 402 challenge
669
+ (NO body in response) (browser signs, retries)
670
+ ```
671
+
672
+ ### Frontend pattern (React)
673
+
674
+ ```tsx
675
+ function NibgateUnlock({ resource }) {
676
+ const [content, setContent] = useState('');
677
+
678
+ // On mount: check stored proof → get content or show unlock
679
+ useEffect(() => {
680
+ fetch('/api/nibgate/access', {
681
+ headers: { 'x-nibgate-payment-proof': storedProof }
682
+ }).then(r => r.json()).then(data => {
683
+ if (data.content) setContent(data.content);
684
+ });
685
+ }, []);
686
+
687
+ if (content) return <div>{content}</div>; // Already paid → show content
688
+ return <button onClick={handleUnlock}>Pay</button>; // Need payment
689
+ }
690
+ ```
691
+
692
+ ### Backend pattern (Express)
693
+
694
+ ```js
695
+ // Public — NEVER returns body for paid content
696
+ app.get('/posts/:slug', async (req, res) => {
697
+ const post = await db.post.findUnique({ where: { slug } });
698
+ if (post.price) {
699
+ const { body, ...teaser } = post;
700
+ return res.json({ post: { ...teaser, isLocked: true } }); // NO body
701
+ }
702
+ res.json({ post }); // free post → body OK
703
+ });
704
+
705
+ // Protected — returns body after proof verification
706
+ app.get('/nibgate/access', async (req, res) => {
707
+ const access = server.accessFor(request, resource);
708
+ if (access.allowed) {
709
+ const post = await db.post.findUnique({ where: { slug } });
710
+ return res.json({ ok: true, content: post.body }); // body ONLY here
711
+ }
712
+ // ... 402 flow
713
+ });
714
+ ```
715
+
716
+ ### Key rules
717
+
718
+ - **Never** include paid content in the initial HTML, even with `hidden` or `display:none`
719
+ - **Never** return the body from a public API endpoint for paid posts
720
+ - **Always** serve content through the protected access endpoint after proof verification
721
+ - **Always** use viem's `walletClient.signTypedData()` with MetaMask, not raw `eth_signTypedData_v4`
722
+
723
+ ## 17. Framework Notes
634
724
 
635
725
  - **Next.js**: Use route handlers for `/nibgate.json` and `/api/nibgate/access`. Protect content before rendering paid payloads. Use `server-only` for private modules.
636
726
  - **Express/NestJS**: Use middleware, controllers, or guards around protected routes. Mount the admin router at `/admin/nibgate`.