@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 +74 -2
- package/SKILL.md +93 -3
- package/dist/nibgate.js +782 -751
- package/dist/nibgate.js.map +4 -4
- package/dist/nibgate.min.js +76 -81
- package/dist/nibgate.min.js.map +4 -4
- package/package.json +1 -1
- package/src/browser/default-ui.js +79 -56
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?`
|
|
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 `
|
|
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.
|
|
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`.
|