@nibgate/sdk 0.1.7 → 0.1.9
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/SKILL.md +306 -63
- package/package.json +1 -1
- package/src/browser/index.js +63 -0
- package/src/browser/storage.js +2 -1
- package/src/index.d.ts +14 -0
- package/src/server/access.js +1 -1
- package/src/server/admin-store.js +60 -0
- package/src/server/admin.js +261 -0
- package/src/server/index.js +2 -0
- package/src/server.d.ts +22 -0
package/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nibgate-sdk
|
|
3
|
-
description:
|
|
3
|
+
description: Complete guide for integrating @nibgate/sdk into a creator-owned site. Covers widget installation, resource definition, server gating, payments (Circle Gateway/x402 on Arc Testnet), browser unlock UI, admin panel for managing gating settings, onchain reputation/ratings, manifest/discovery metadata, and common gotchas.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Nibgate SDK
|
|
7
7
|
|
|
8
|
-
Use `@nibgate/sdk` in the creator-owned site that
|
|
8
|
+
Use `@nibgate/sdk` in the creator-owned site that serves the content. The Nibgate Hub verifies the site, indexes public metadata, and records events. The creator site keeps the protected payload, payment receiver logic, and admin configuration.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -20,16 +20,11 @@ When an AI agent is helping install Nibgate, give it this file first:
|
|
|
20
20
|
- package copy: `node_modules/@nibgate/sdk/SKILL.md`
|
|
21
21
|
- public copy: `https://nibgate.xyz/skill.md`
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Read this entire file before editing any routes or checkout code. The guide follows the full integration order.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
import { checkResourceAccess, setupResourcePage, trackResourcePage } from '@nibgate/sdk'
|
|
29
|
-
import { createCircleGatewayServer, manifestResponse } from '@nibgate/sdk/server'
|
|
30
|
-
```
|
|
25
|
+
---
|
|
31
26
|
|
|
32
|
-
## Site Widget
|
|
27
|
+
## 1. Site Widget (Analytics & Ownership Proof)
|
|
33
28
|
|
|
34
29
|
Paste the dashboard widget into the verified site layout:
|
|
35
30
|
|
|
@@ -41,9 +36,13 @@ Paste the dashboard widget into the verified site layout:
|
|
|
41
36
|
</script>
|
|
42
37
|
```
|
|
43
38
|
|
|
44
|
-
The widget proves ownership
|
|
39
|
+
**The widget is ONLY for analytics.** It proves domain ownership, detects resources from `data-nibgate-resource` attributes or meta tags, tracks page views, scroll depth, and time spent. It does NOT pop up a wallet or handle checkout. Wallet/payment logic comes from the SDK browser helpers.
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
If the widget loads after app code, the SDK queues events and flushes them when the widget is ready.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Resource Shape
|
|
47
46
|
|
|
48
47
|
Define one stable resource per paid route, CMS row, media item, file, or API product:
|
|
49
48
|
|
|
@@ -64,11 +63,15 @@ const resource = {
|
|
|
64
63
|
}
|
|
65
64
|
```
|
|
66
65
|
|
|
67
|
-
Allowed
|
|
66
|
+
Allowed types: `article`, `image`, `music`, `video` (aliases like `audio→music`, `photo→image` are normalized). Use stable IDs — changing IDs breaks continuity for Explore, receipts, and reputation.
|
|
67
|
+
|
|
68
|
+
**Important:** The `image`, `description`, and `title` fields become the public thumbnail and card copy on the Explore page. Use a teaser preview, not the actual paid file.
|
|
69
|
+
|
|
70
|
+
---
|
|
68
71
|
|
|
69
|
-
## Discovery Metadata
|
|
72
|
+
## 3. Discovery Metadata (nibgate.json)
|
|
70
73
|
|
|
71
|
-
Expose public metadata at `/nibgate.json
|
|
74
|
+
Expose public metadata at `/nibgate.json` so the Hub can index your content:
|
|
72
75
|
|
|
73
76
|
```ts
|
|
74
77
|
import { manifestResponse } from '@nibgate/sdk/server'
|
|
@@ -83,9 +86,49 @@ export function GET() {
|
|
|
83
86
|
|
|
84
87
|
Never include the protected payload in `nibgate.json`. It is only for public cards, indexing, and agent-readable discovery.
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 4. Server Access Route (Payment Challenge & Unlock)
|
|
92
|
+
|
|
93
|
+
Protect paid content in a server route, API handler, or middleware:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createCircleGatewayServer } from '@nibgate/sdk/server'
|
|
97
|
+
|
|
98
|
+
const nibgate = createCircleGatewayServer({
|
|
99
|
+
origin: process.env.NIBGATE_SITE_ORIGIN,
|
|
100
|
+
secret: process.env.NIBGATE_SECRET,
|
|
101
|
+
network: process.env.NIBGATE_PAYMENT_NETWORK || 'eip155:5042002'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export function GET(request: Request) {
|
|
105
|
+
return nibgate.accessResponse(request, resource, {
|
|
106
|
+
getContent: async () => new Response(protectedHtml, {
|
|
107
|
+
headers: { 'content-type': 'text/html; charset=utf-8' }
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**How `accessResponse` works:**
|
|
114
|
+
1. Checks for an existing unlock proof via `x-nibgate-payment-proof` header
|
|
115
|
+
2. If valid → returns content (200)
|
|
116
|
+
3. If not and `humans: 'blocked'` → returns 403
|
|
117
|
+
4. If not and `humans: 'paid'` → returns a `402 Payment Required` challenge with the creator's wallet, price, and network
|
|
118
|
+
5. If the request has a `payment-signature` header AND `NIBGATE_PAYMENT_MODE=circle-gateway` is set → verifies the payment and returns unlock proof + content
|
|
119
|
+
|
|
120
|
+
**The `accessPath` is queried exactly as-is.** The SDK does not auto-append `?id=` or route params. If your access route needs a content identifier, include it in the path (e.g., `/api/nibgate/access/my-content-id`) or pass it as a query param in the resource's `accessPath` field.
|
|
121
|
+
|
|
122
|
+
**Custom backends:** If you build your own access route instead of using `accessResponse()`, your route must:
|
|
123
|
+
- Return 402 with the challenge JSON when no payment proof is present
|
|
124
|
+
- Check for `payment-signature` header — if present and valid, return 200 with `{ ok: true, unlockProof: "...", payment: {...} }`
|
|
125
|
+
- Check for `x-nibgate-payment-proof` header — if present and valid, return 200 with the protected content
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 5. Browser Page Wiring (Unlock UI)
|
|
87
130
|
|
|
88
|
-
Track the resource page and wire unlock
|
|
131
|
+
Track the resource page and wire the unlock button:
|
|
89
132
|
|
|
90
133
|
```ts
|
|
91
134
|
import { setupResourcePage, trackResourcePage } from '@nibgate/sdk'
|
|
@@ -102,52 +145,244 @@ setupResourcePage(resource, {
|
|
|
102
145
|
})
|
|
103
146
|
```
|
|
104
147
|
|
|
105
|
-
For
|
|
148
|
+
For the browser wallet checkout flow (user signs with MetaMask):
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { createEvmGatewayUnlock } from '@nibgate/sdk'
|
|
152
|
+
|
|
153
|
+
createEvmGatewayUnlock(resource, {
|
|
154
|
+
accessPath: '/api/nibgate/access',
|
|
155
|
+
unlockButton: '#unlock-btn',
|
|
156
|
+
status: '#status',
|
|
157
|
+
connectButton: '#connect-btn'
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The browser flow:
|
|
162
|
+
1. `checkResourceAccess()` calls the access route
|
|
163
|
+
2. If 402 → prompts wallet to sign the Gateway payment
|
|
164
|
+
3. Sends signature back to access route
|
|
165
|
+
4. Server verifies and returns unlock proof
|
|
166
|
+
5. Proof stored in localStorage → subsequent requests include it as `x-nibgate-payment-proof`
|
|
106
167
|
|
|
107
|
-
|
|
168
|
+
For custom flows, use `checkResourceAccess(resource, options)` directly.
|
|
108
169
|
|
|
109
|
-
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 6. Server-Side Pay Route (Alternative to Browser Wallet)
|
|
173
|
+
|
|
174
|
+
For testing or server-driven payments, add a pay endpoint that uses the server's buyer key:
|
|
110
175
|
|
|
111
176
|
```ts
|
|
112
|
-
import {
|
|
177
|
+
import { nibgateServer } from './nibgate-resource'
|
|
113
178
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
179
|
+
export function POST({ request }) {
|
|
180
|
+
return nibgateServer.payAndUnlockResponse(request, resource, {
|
|
181
|
+
origin: process.env.NIBGATE_SITE_ORIGIN,
|
|
182
|
+
accessPath: '/api/nibgate/access'
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This requires `NIBGATE_BUYER_PRIVATE_KEY` to be set. In production, prefer browser wallet checkout so the user pays from their own wallet.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 7. Admin Panel (Manage Gating Settings From Your Site)
|
|
192
|
+
|
|
193
|
+
The SDK ships a ready-to-use admin panel that lets creators manage gating, pricing, and settings from the creator's own site instead of editing code.
|
|
194
|
+
|
|
195
|
+
### Setup
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { createFileStore, createAdminApi } from '@nibgate/sdk/server'
|
|
199
|
+
import express from 'express'
|
|
200
|
+
|
|
201
|
+
const store = createFileStore({ path: './nibgate-settings.json' })
|
|
202
|
+
const admin = createAdminApi({ store })
|
|
203
|
+
|
|
204
|
+
const app = express()
|
|
205
|
+
app.use(express.json())
|
|
206
|
+
|
|
207
|
+
// Mount admin API routes (list, get, update, delete resource settings)
|
|
208
|
+
app.use(admin.router(express))
|
|
209
|
+
|
|
210
|
+
// Or use individual handlers for non-Express frameworks:
|
|
211
|
+
// app.get('/admin/nibgate/resources', admin.handleList)
|
|
212
|
+
// app.post('/admin/nibgate/resources/:id', admin.handleUpdate)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Protect the admin route
|
|
216
|
+
|
|
217
|
+
Pass an `authorize` function to `createAdminApi`:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const admin = createAdminApi({
|
|
221
|
+
store,
|
|
222
|
+
authorize: (req) => {
|
|
223
|
+
// Check session, API key, admin token, etc.
|
|
224
|
+
return req.headers['x-admin-key'] === process.env.ADMIN_KEY
|
|
225
|
+
}
|
|
118
226
|
})
|
|
227
|
+
```
|
|
119
228
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
229
|
+
### Using settings in your resource definitions
|
|
230
|
+
|
|
231
|
+
The admin panel stores per-resource settings. Build resource objects from stored settings:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { settingsToAccessPolicy, settingsToUnlockPolicy } from '@nibgate/sdk/server'
|
|
235
|
+
|
|
236
|
+
function getResource(id) {
|
|
237
|
+
const settings = store.get(id) || {}
|
|
238
|
+
return {
|
|
239
|
+
id,
|
|
240
|
+
title: settings.title || post.title,
|
|
241
|
+
type: settings.type || 'article',
|
|
242
|
+
price: settings.price || '0.01',
|
|
243
|
+
currency: settings.currency || 'USDC',
|
|
244
|
+
recipient: settings.recipient || process.env.NIBGATE_SELLER_ADDRESS,
|
|
245
|
+
access: settingsToAccessPolicy(settings),
|
|
246
|
+
unlock: settingsToUnlockPolicy(settings),
|
|
247
|
+
ratingsEnabled: settings.ratingsEnabled !== false
|
|
248
|
+
}
|
|
126
249
|
}
|
|
127
250
|
```
|
|
128
251
|
|
|
129
|
-
|
|
252
|
+
### Admin page
|
|
130
253
|
|
|
131
|
-
|
|
254
|
+
Visit `/admin/nibgate` in your browser. The page shows all configured resources with controls for:
|
|
255
|
+
- Human access (free / paid / blocked)
|
|
256
|
+
- Agent access (free / paid / blocked)
|
|
257
|
+
- Unlock mode
|
|
258
|
+
- Payment rail
|
|
259
|
+
- Price & currency
|
|
260
|
+
- Recipient wallet
|
|
261
|
+
- Ratings toggle
|
|
262
|
+
- Content type
|
|
263
|
+
- Publish to discovery toggle
|
|
132
264
|
|
|
133
|
-
|
|
265
|
+
---
|
|
134
266
|
|
|
135
|
-
|
|
267
|
+
## 8. Payments (Circle Gateway / x402 on Arc Testnet)
|
|
136
268
|
|
|
137
|
-
|
|
138
|
-
2. keep the full paid body in a server-only module, route handler, CMS fetch, DB row, private file, or signed URL endpoint;
|
|
139
|
-
3. run `accessResponse(...)`, `accessFor(...)`, middleware, or a framework guard before returning the full payload.
|
|
269
|
+
For v1, treat `unlock.mode: 'one_time'` with Circle Gateway/x402 as the production path. All payments settle on Arc Testnet (chain ID 5042002) in USDC.
|
|
140
270
|
|
|
141
|
-
|
|
271
|
+
### How payment works
|
|
142
272
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
273
|
+
1. Browser requests access → server returns 402 challenge with creator's wallet, price, network
|
|
274
|
+
2. Browser prompts user to sign a Gateway payment payload (EIP-712 typed data via `eth_signTypedData_v4`)
|
|
275
|
+
3. Signed payload sent to server as `payment-signature` header
|
|
276
|
+
4. Server verifies with `createGatewayMiddleware` from `@circle-fin/x402-batching/server`
|
|
277
|
+
5. On success, server creates an HMAC-signed unlock token and returns it
|
|
278
|
+
6. Browser stores the token as `x-nibgate-payment-proof` for subsequent requests
|
|
147
279
|
|
|
148
|
-
|
|
280
|
+
### Before testing locally
|
|
281
|
+
|
|
282
|
+
Deposit testnet USDC from your wallet into the Gateway facilitator so transactions can settle:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npx nibgate deposit 10
|
|
286
|
+
```
|
|
149
287
|
|
|
150
|
-
|
|
288
|
+
Check your Gateway balance:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx nibgate balance
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Env vars
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
NIBGATE_PAYMENT_MODE=circle-gateway
|
|
298
|
+
NIBGATE_PAYMENT_NETWORK=eip155:5042002
|
|
299
|
+
NIBGATE_SELLER_ADDRESS=0xCreatorReceiver
|
|
300
|
+
NIBGATE_BUYER_PRIVATE_KEY=0x... # Only for local testing
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Important
|
|
304
|
+
|
|
305
|
+
- A Gateway/x402 payment signature is a valid payment proof. A normal wallet message signature (`eth_sign` / `personal_sign`) is NOT payment — do not accept it.
|
|
306
|
+
- Use `NIBGATE_BUYER_PRIVATE_KEY` only in local demos. In production, the visitor or agent wallet signs the payment challenge.
|
|
307
|
+
- The `NIBGATE_SECRET` used by `createNibgateServer()` MUST match the secret used by your access and pay routes. If they diverge, unlock tokens signed by one route fail verification on the other. Set once, use everywhere.
|
|
308
|
+
|
|
309
|
+
### For plain HTML/JS sites without a bundler
|
|
310
|
+
|
|
311
|
+
The SDK relies on bare module imports (`viem`, `@x402/core`). These won't work in the browser without a bundler. Compile a browser-ready bundle:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
esbuild --bundle --format=iife --global-name=Nibgate \
|
|
315
|
+
--outfile=nibgate-bundle.js \
|
|
316
|
+
node_modules/@nibgate/sdk/src/browser/index.js
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Then load it:
|
|
320
|
+
|
|
321
|
+
```html
|
|
322
|
+
<script src="/nibgate-bundle.js"></script>
|
|
323
|
+
<script>
|
|
324
|
+
const { nibgate, gate } = Nibgate
|
|
325
|
+
</script>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Smart Contract Wallets
|
|
329
|
+
|
|
330
|
+
Some smart contract wallets (Gnosis Safe, Argent, etc.) may not support `eth_signTypedData_v4` required by the Gateway payment signing. Test with an EOA (MetaMask, Rabby, Coinbase Wallet) first. If your users primarily use smart contract wallets, use the server-side pay route (`payAndUnlockResponse`) with `NIBGATE_BUYER_PRIVATE_KEY` instead.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 9. Onchain Reputation & Ratings
|
|
335
|
+
|
|
336
|
+
After a verified unlock, users can rate content onchain via the `NibgateReputation.sol` contract deployed on Arc Testnet.
|
|
337
|
+
|
|
338
|
+
### How it works
|
|
339
|
+
|
|
340
|
+
1. User unlocks content (has a valid unlock proof)
|
|
341
|
+
2. rating UI calls `rateContentOnchain(resource, { rating, paymentId })`
|
|
342
|
+
3. User signs an onchain transaction with their rating
|
|
343
|
+
4. Transaction indexed by Nibgate's reputation indexer (polls every 30s)
|
|
344
|
+
5. Rating contributes to content/creator reputation scores on Explore and leaderboards
|
|
345
|
+
|
|
346
|
+
### Browser rating UI
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { createOnchainRating } from '@nibgate/sdk'
|
|
350
|
+
|
|
351
|
+
createOnchainRating(resource, {
|
|
352
|
+
ratingButtons: '[data-rating]',
|
|
353
|
+
status: '#rating-status',
|
|
354
|
+
payment: latestPayment // from the unlock result
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Or buttons with `data-nibgate-rating-value="4"` attributes. Ratings are 1-5.
|
|
359
|
+
|
|
360
|
+
### Reputation requirements
|
|
361
|
+
|
|
362
|
+
- Only ratings from wallets that have an unlock receipt for the same content are counted
|
|
363
|
+
- Ratings are onchain (requires gas on Arc Testnet)
|
|
364
|
+
- Content reputation (0-5 stars) and creator reputation (0-100 score) update after indexing
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 10. Hardcoded Or File-Based Content
|
|
369
|
+
|
|
370
|
+
Hardcoded content, MDX files, Markdown files, static JSON files, and repo-local media can be gated when the protected payload stays server-side until access is allowed.
|
|
371
|
+
|
|
372
|
+
**Safe pattern:**
|
|
373
|
+
1. Keep the public teaser, title, price, and metadata in the page
|
|
374
|
+
2. Keep the full paid body in a server-only module, route handler, CMS fetch, DB row, private file, or signed URL endpoint
|
|
375
|
+
3. Run `accessResponse()`, `accessFor()`, middleware, or a framework guard before returning the full payload
|
|
376
|
+
|
|
377
|
+
**Unsafe pattern (will not protect your content):**
|
|
378
|
+
- Rendering the paid body into public HTML and hiding it with CSS
|
|
379
|
+
- Bundling the paid body into client JavaScript
|
|
380
|
+
- Putting private text into `nibgate.json`, JSON-LD, meta tags, page source, or static exports
|
|
381
|
+
- Exposing an open API route that returns the protected body without checking payment proof
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 11. Required Env Vars
|
|
151
386
|
|
|
152
387
|
```bash
|
|
153
388
|
NIBGATE_SITE_ORIGIN=https://creator.example
|
|
@@ -160,30 +395,38 @@ NIBGATE_PAYMENT_NETWORK=eip155:5042002
|
|
|
160
395
|
NIBGATE_SELLER_ADDRESS=0xCreatorReceiver
|
|
161
396
|
```
|
|
162
397
|
|
|
163
|
-
Only expose values with `NEXT_PUBLIC_` or client-side env names when they are intentionally public
|
|
398
|
+
Only expose values with `NEXT_PUBLIC_` or client-side env names when they are intentionally public (site id, public token, API base, reputation contract address). Never ship `NIBGATE_SECRET`, private keys, R2 credentials, Resend keys, database URLs, or privileged payment credentials to browser code.
|
|
164
399
|
|
|
165
|
-
|
|
400
|
+
---
|
|
166
401
|
|
|
167
|
-
|
|
402
|
+
## 12. Common Gotchas
|
|
168
403
|
|
|
169
|
-
|
|
404
|
+
- **Widget is only for analytics.** It does not pop up wallets or handle checkout. Use the SDK browser helpers for that.
|
|
405
|
+
- **accessPath is literal.** The SDK does not append `?id=` or route params. Include identifiers in the path or query string manually.
|
|
406
|
+
- **Custom backends must handle `payment-signature` header.** If you build your own access route without `accessResponse()`, your route must detect `payment-signature` header and return 200 on valid proof, not 402.
|
|
407
|
+
- **Cookie `[object Object]` bug.** The unlock proof must be a string (the token from `createUnlockToken`), not an object. `storePaymentProof` now handles both, but ensure your server returns `unlockProof` as a string.
|
|
408
|
+
- **Secrets must match.** `NIBGATE_SECRET` must be the same in `createNibgateServer()`, the access route, and the pay route. Mismatch causes unlock tokens to fail verification silently.
|
|
409
|
+
- **Smart contract wallets** may not support `eth_signTypedData_v4`. Use an EOA or the server-side pay route.
|
|
410
|
+
- **Wallet extension conflicts** can cause signature corruption if multiple wallets are installed. Advise users to disable other wallet extensions when testing.
|
|
411
|
+
- **Deposit before testing locally.** Run `npx nibgate deposit 10` to fund the Gateway facilitator balance.
|
|
170
412
|
|
|
171
|
-
|
|
413
|
+
---
|
|
172
414
|
|
|
173
|
-
|
|
415
|
+
## 13. Never Do These
|
|
174
416
|
|
|
175
|
-
|
|
417
|
+
- Do not hide paid HTML with CSS or client state after rendering it publicly
|
|
418
|
+
- Do not fake payment IDs, receipts, unlocks, or successful access
|
|
419
|
+
- Do not replace Gateway/x402 payment signatures with ordinary wallet message signatures
|
|
420
|
+
- Do not store backend secrets in browser env vars
|
|
421
|
+
- Do not let mutable titles or slugs be the only resource identity
|
|
422
|
+
- Do not expose private content in `nibgate.json`, meta tags, JSON-LD, page source, or static builds
|
|
176
423
|
|
|
177
|
-
|
|
178
|
-
- Do not fake payment ids, receipts, unlocks, or successful access.
|
|
179
|
-
- Do not replace Gateway/x402 payment signatures with ordinary wallet message signatures.
|
|
180
|
-
- Do not store backend secrets in browser env vars.
|
|
181
|
-
- Do not let mutable titles or slugs be the only resource identity.
|
|
182
|
-
- Do not expose private content in `nibgate.json`, meta tags, JSON-LD, page source, or static builds.
|
|
424
|
+
---
|
|
183
425
|
|
|
184
|
-
## Framework Notes
|
|
426
|
+
## 14. Framework Notes
|
|
185
427
|
|
|
186
|
-
- **Next.js**:
|
|
187
|
-
- **Express/NestJS**:
|
|
188
|
-
- **Astro/SvelteKit/Remix**:
|
|
189
|
-
- **CMS apps**:
|
|
428
|
+
- **Next.js**: Use route handlers for `/nibgate.json` and `/api/nibgate/access`. Protect content before rendering paid payloads. Use `server-only` for private modules.
|
|
429
|
+
- **Express/NestJS**: Use middleware, controllers, or guards around protected routes. Mount the admin router at `/admin/nibgate`.
|
|
430
|
+
- **Astro/SvelteKit/Remix**: Use SSR/server routes or endpoints. Plain static builds need a protected API or signed URL for private payloads.
|
|
431
|
+
- **CMS apps**: Save Nibgate resource settings beside each content record. Use `settingsToAccessPolicy()` and `settingsToUnlockPolicy()` to convert stored settings to resource shapes. Mount the admin panel for easy management.
|
|
432
|
+
- **Static/vanilla HTML/JS**: Compile the SDK into a bundle with esbuild (see section 8). Use `gate()` and `nibgate` from the global `Nibgate` namespace.
|
package/package.json
CHANGED
package/src/browser/index.js
CHANGED
|
@@ -556,6 +556,66 @@ export function createOnchainRating(resource, options = {}) {
|
|
|
556
556
|
return controller;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
export function mountRatingUI(resource, options = {}) {
|
|
560
|
+
const item = createGate(resource, options.gateOptions || {});
|
|
561
|
+
const win = browserWindow();
|
|
562
|
+
if (!win) return null;
|
|
563
|
+
const target = typeof options.target === 'string' ? win.document.querySelector(options.target) : options.target;
|
|
564
|
+
if (!target) return null;
|
|
565
|
+
|
|
566
|
+
const stars = [1, 2, 3, 4, 5];
|
|
567
|
+
let selectedRating = 0;
|
|
568
|
+
|
|
569
|
+
const container = win.document.createElement('div');
|
|
570
|
+
container.className = 'nibgate-rating-ui';
|
|
571
|
+
container.style.cssText = 'display:flex;align-items:center;gap:4px;padding:8px 0';
|
|
572
|
+
|
|
573
|
+
const starButtons = stars.map((value) => {
|
|
574
|
+
const btn = win.document.createElement('button');
|
|
575
|
+
btn.type = 'button';
|
|
576
|
+
btn.dataset.nibgateRatingValue = String(value);
|
|
577
|
+
btn.setAttribute('aria-label', `${value} star${value > 1 ? 's' : ''}`);
|
|
578
|
+
btn.innerHTML = '☆';
|
|
579
|
+
btn.style.cssText = 'background:none;border:none;font-size:24px;cursor:pointer;color:#ccc;transition:color 0.15s;padding:2px;line-height:1';
|
|
580
|
+
btn.addEventListener('mouseenter', () => {
|
|
581
|
+
starButtons.forEach((b, i) => b.style.color = i < value ? '#f5b342' : '#ccc');
|
|
582
|
+
});
|
|
583
|
+
btn.addEventListener('mouseleave', () => {
|
|
584
|
+
starButtons.forEach((b, i) => b.style.color = i < selectedRating ? '#f5b342' : '#ccc');
|
|
585
|
+
});
|
|
586
|
+
btn.addEventListener('click', () => {
|
|
587
|
+
selectedRating = value;
|
|
588
|
+
starButtons.forEach((b, i) => b.style.color = i < value ? '#f5b342' : '#ccc');
|
|
589
|
+
rate(item.resource, { rating: value }).catch(() => {});
|
|
590
|
+
});
|
|
591
|
+
container.appendChild(btn);
|
|
592
|
+
return btn;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const statusEl = win.document.createElement('span');
|
|
596
|
+
statusEl.style.cssText = 'font-size:13px;color:#888;margin-left:8px';
|
|
597
|
+
statusEl.textContent = options.label || 'Rate this content';
|
|
598
|
+
container.appendChild(statusEl);
|
|
599
|
+
|
|
600
|
+
target.appendChild(container);
|
|
601
|
+
|
|
602
|
+
function rate(r, input = {}) {
|
|
603
|
+
return item.rate({ ...input, rating: r });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function setRating(value) {
|
|
607
|
+
selectedRating = value;
|
|
608
|
+
starButtons.forEach((b, i) => b.style.color = i < value ? '#f5b342' : '#ccc');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
resource: item.resource,
|
|
613
|
+
container,
|
|
614
|
+
setRating,
|
|
615
|
+
rate
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
559
619
|
export async function payAndUnlockResource(resource, options = {}) {
|
|
560
620
|
const item = createGate(resource, options.gateOptions || {});
|
|
561
621
|
const payPath = options.payPath || item.resource.payPath || '/api/nibgate/pay';
|
|
@@ -690,6 +750,9 @@ export function createNibgate(defaults = {}) {
|
|
|
690
750
|
createOnchainRating(resource, options = {}) {
|
|
691
751
|
return createOnchainRating(resourceWithDefaults(resource), options);
|
|
692
752
|
},
|
|
753
|
+
mountRatingUI(resource, options = {}) {
|
|
754
|
+
return mountRatingUI(resourceWithDefaults(resource), options);
|
|
755
|
+
},
|
|
693
756
|
payAndUnlockResource(resource, options = {}) {
|
|
694
757
|
return payAndUnlockResource(resourceWithDefaults(resource), options);
|
|
695
758
|
},
|
package/src/browser/storage.js
CHANGED
|
@@ -27,7 +27,8 @@ export function storePaymentProof(resource, proof) {
|
|
|
27
27
|
const win = browserWindow();
|
|
28
28
|
if (!win || !proof) return false;
|
|
29
29
|
try {
|
|
30
|
-
|
|
30
|
+
const value = typeof proof === 'string' ? proof : JSON.stringify(proof);
|
|
31
|
+
win.localStorage.setItem(proofStorageKey(resource), value);
|
|
31
32
|
return true;
|
|
32
33
|
} catch (_error) {
|
|
33
34
|
return false;
|
package/src/index.d.ts
CHANGED
|
@@ -114,6 +114,7 @@ export type NibgateClient = {
|
|
|
114
114
|
createEvmGatewayUnlock(resource: NibgateResource | string, options?: NibgateEvmGatewayUnlockOptions): NibgateEvmGatewayUnlockController;
|
|
115
115
|
rateContentOnchain(resource: NibgateResource | string, options: NibgateOnchainRatingOptions): Promise<NibgateOnchainRatingResult>;
|
|
116
116
|
createOnchainRating(resource: NibgateResource | string, options?: NibgateOnchainRatingUiOptions): NibgateOnchainRatingController;
|
|
117
|
+
mountRatingUI(resource: NibgateResource | string, options?: NibgateRatingUiOptions): NibgateRatingUiController | null;
|
|
117
118
|
payAndUnlockResource(resource: NibgateResource | string, options?: NibgatePaymentOptions): Promise<NibgatePaymentResult>;
|
|
118
119
|
setupResourcePage(resource: NibgateResource | string, options?: NibgatePageSetupOptions): NibgateGate;
|
|
119
120
|
ratingMessage(resource: NibgateResource | string, rating?: NibgateRating | number, options?: Record<string, unknown>): string;
|
|
@@ -430,5 +431,18 @@ export declare function ratingMessage(resource: NibgateResource | string, rating
|
|
|
430
431
|
export declare function createNibgate(defaults?: { resource?: NibgateResource }): NibgateClient;
|
|
431
432
|
export declare const nibgate: NibgateClient;
|
|
432
433
|
|
|
434
|
+
export interface NibgateRatingUiOptions {
|
|
435
|
+
target?: string | HTMLElement;
|
|
436
|
+
label?: string;
|
|
437
|
+
gateOptions?: Record<string, unknown>;
|
|
438
|
+
}
|
|
439
|
+
export interface NibgateRatingUiController {
|
|
440
|
+
resource: NibgateResource;
|
|
441
|
+
container: HTMLElement;
|
|
442
|
+
setRating(value: number): void;
|
|
443
|
+
rate(resource: NibgateResource | string, input?: Record<string, unknown>): ReturnType<typeof rateContentOnchain>;
|
|
444
|
+
}
|
|
445
|
+
export declare function mountRatingUI(resource: NibgateResource | string, options?: NibgateRatingUiOptions): NibgateRatingUiController | null;
|
|
446
|
+
|
|
433
447
|
export declare function createTransferCheckout(resource: NibgateResource | string, options: NibgateTransferCheckoutOptions): NibgateTransferCheckout;
|
|
434
448
|
export declare function payWithTransfer(resource: NibgateResource | string, options: NibgateTransferCheckoutOptions & NibgateAccessCheckOptions): Promise<NibgateAccessCheckResult>;
|
package/src/server/access.js
CHANGED
|
@@ -104,7 +104,7 @@ export function createNibgateServer(options = {}) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const rail = normalizePaymentRail(resource.paymentRail || routeOptions.paymentRail || options.paymentRail || routeOptions.paymentMode || options.paymentMode);
|
|
107
|
-
if (rail === 'gateway' && (routeOptions.paymentMode || options.paymentMode || serverEnv('NIBGATE_PAYMENT_MODE')
|
|
107
|
+
if (rail === 'gateway' && (routeOptions.paymentMode || options.paymentMode || serverEnv('NIBGATE_PAYMENT_MODE')) === 'circle-gateway') {
|
|
108
108
|
const gateway = await runCircleGatewayRequirement(request, resource, { ...options, ...routeOptions });
|
|
109
109
|
if (gateway.handled) return gateway.response;
|
|
110
110
|
const result = await unlock(resource, gateway.payment);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { serverEnv } from './env.js';
|
|
4
|
+
|
|
5
|
+
export function createFileStore(options = {}) {
|
|
6
|
+
const filePath = path.resolve(options.path || serverEnv('NIBGATE_ADMIN_STORE_PATH') || './nibgate-settings.json');
|
|
7
|
+
|
|
8
|
+
function read() {
|
|
9
|
+
try {
|
|
10
|
+
if (!fs.existsSync(filePath)) return {};
|
|
11
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function write(data) {
|
|
18
|
+
const dir = path.dirname(filePath);
|
|
19
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
list() {
|
|
25
|
+
const data = read();
|
|
26
|
+
return Object.entries(data).map(([id, settings]) => ({ id, ...settings }));
|
|
27
|
+
},
|
|
28
|
+
get(id) {
|
|
29
|
+
const data = read();
|
|
30
|
+
return data[id] || null;
|
|
31
|
+
},
|
|
32
|
+
set(id, settings) {
|
|
33
|
+
const data = read();
|
|
34
|
+
data[id] = { ...(data[id] || {}), ...settings, updatedAt: new Date().toISOString() };
|
|
35
|
+
write(data);
|
|
36
|
+
return data[id];
|
|
37
|
+
},
|
|
38
|
+
remove(id) {
|
|
39
|
+
const data = read();
|
|
40
|
+
delete data[id];
|
|
41
|
+
write(data);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createMemoryStore() {
|
|
48
|
+
const store = {};
|
|
49
|
+
return {
|
|
50
|
+
list() {
|
|
51
|
+
return Object.entries(store).map(([id, settings]) => ({ id, ...settings }));
|
|
52
|
+
},
|
|
53
|
+
get(id) { return store[id] || null; },
|
|
54
|
+
set(id, settings) {
|
|
55
|
+
store[id] = { ...(store[id] || {}), ...settings, updatedAt: new Date().toISOString() };
|
|
56
|
+
return store[id];
|
|
57
|
+
},
|
|
58
|
+
remove(id) { delete store[id]; return true; }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { jsonResponse } from './response.js';
|
|
2
|
+
import { createNibgateContentSettings, settingsToAccessPolicy, settingsToUnlockPolicy, NIBGATE_CONTENT_SETTING_FIELDS } from '../core/settings.js';
|
|
3
|
+
import { normalizeServerResource as normalizeResource } from '../core/resource.js';
|
|
4
|
+
|
|
5
|
+
export function createAdminApi(options = {}) {
|
|
6
|
+
const store = options.store;
|
|
7
|
+
if (!store) throw new Error('Admin API requires a store. Pass one from createFileStore() or createMemoryStore().');
|
|
8
|
+
|
|
9
|
+
function requireAdmin(req, _res) {
|
|
10
|
+
if (typeof options.authorize === 'function') return options.authorize(req);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function handleList(req, res) {
|
|
15
|
+
if (!requireAdmin(req, res)) return jsonResponse({ error: 'Unauthorized' }, { status: 403 });
|
|
16
|
+
const all = store.list();
|
|
17
|
+
return jsonResponse({ success: true, fields: NIBGATE_CONTENT_SETTING_FIELDS, items: all });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function handleGet(req, res) {
|
|
21
|
+
if (!requireAdmin(req, res)) return jsonResponse({ error: 'Unauthorized' }, { status: 403 });
|
|
22
|
+
const item = store.get(req.params.id);
|
|
23
|
+
if (!item) return jsonResponse({ error: 'Not found' }, { status: 404 });
|
|
24
|
+
return jsonResponse({ success: true, fields: NIBGATE_CONTENT_SETTING_FIELDS, item });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function handleUpdate(req, res) {
|
|
28
|
+
if (!requireAdmin(req, res)) return jsonResponse({ error: 'Unauthorized' }, { status: 403 });
|
|
29
|
+
const settings = createNibgateContentSettings(req.body || {});
|
|
30
|
+
const saved = store.set(req.params.id, settings);
|
|
31
|
+
return jsonResponse({ success: true, item: saved });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handleDelete(req, res) {
|
|
35
|
+
if (!requireAdmin(req, res)) return jsonResponse({ error: 'Unauthorized' }, { status: 403 });
|
|
36
|
+
store.remove(req.params.id);
|
|
37
|
+
return jsonResponse({ success: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildResourceFromSettings(id, settings) {
|
|
41
|
+
const access = settingsToAccessPolicy(settings);
|
|
42
|
+
const unlock = settingsToUnlockPolicy(settings);
|
|
43
|
+
return normalizeResource({
|
|
44
|
+
id,
|
|
45
|
+
type: settings.type,
|
|
46
|
+
price: settings.price,
|
|
47
|
+
currency: settings.currency,
|
|
48
|
+
recipient: settings.recipient,
|
|
49
|
+
access,
|
|
50
|
+
unlock,
|
|
51
|
+
ratingsEnabled: settings.ratingsEnabled,
|
|
52
|
+
license: settings.license
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function router(express) {
|
|
57
|
+
const Route = express?.Router ? express.Router() : null;
|
|
58
|
+
if (!Route) return null;
|
|
59
|
+
|
|
60
|
+
Route.get('/admin/nibgate/resources', handleList);
|
|
61
|
+
Route.get('/admin/nibgate/resources/:id', handleGet);
|
|
62
|
+
Route.post('/admin/nibgate/resources/:id', handleUpdate);
|
|
63
|
+
Route.delete('/admin/nibgate/resources/:id', handleDelete);
|
|
64
|
+
Route.get('/admin/nibgate', (_req, res) => {
|
|
65
|
+
const html = adminPageHtml({ title: options.title || 'Nibgate Admin' });
|
|
66
|
+
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
67
|
+
res.end(html);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return Route;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
handleList,
|
|
75
|
+
handleGet,
|
|
76
|
+
handleUpdate,
|
|
77
|
+
handleDelete,
|
|
78
|
+
buildResourceFromSettings,
|
|
79
|
+
router,
|
|
80
|
+
store,
|
|
81
|
+
settingsFields: NIBGATE_CONTENT_SETTING_FIELDS
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function adminPageHtml(options = {}) {
|
|
86
|
+
const title = options.title || 'Nibgate Admin';
|
|
87
|
+
const apiBase = options.apiBase || '/admin/nibgate';
|
|
88
|
+
return `<!DOCTYPE html>
|
|
89
|
+
<html lang="en">
|
|
90
|
+
<head>
|
|
91
|
+
<meta charset="UTF-8">
|
|
92
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
93
|
+
<title>${title}</title>
|
|
94
|
+
<style>
|
|
95
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
96
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f0;color:#1a1a18;padding:24px;max-width:960px;margin:0 auto}
|
|
97
|
+
h1{font-size:28px;font-weight:700;margin-bottom:8px}
|
|
98
|
+
p.sub{color:#666;margin-bottom:24px}
|
|
99
|
+
.card{background:#fff;border-radius:12px;padding:20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,0.08)}
|
|
100
|
+
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
|
101
|
+
.card-header h3{font-size:18px;font-weight:600}
|
|
102
|
+
.status{font-size:12px;padding:3px 10px;border-radius:99px;font-weight:600}
|
|
103
|
+
.status-gated{background:#fef3c7;color:#92400e}
|
|
104
|
+
.status-open{background:#d1fae5;color:#065f46}
|
|
105
|
+
.status-hidden{background:#fce4ec;color:#c62828}
|
|
106
|
+
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
|
|
107
|
+
.field{display:flex;flex-direction:column;gap:4px}
|
|
108
|
+
.field label{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#555}
|
|
109
|
+
.field input,.field select{font-size:14px;padding:8px 10px;border:1px solid #ddd;border-radius:6px;background:#fff}
|
|
110
|
+
.field input:focus,.field select:focus{outline:2px solid #7c9a6d;border-color:transparent}
|
|
111
|
+
.field .checkbox-row{display:flex;align-items:center;gap:8px;padding-top:6px}
|
|
112
|
+
.field .checkbox-row input[type="checkbox"]{width:18px;height:18px}
|
|
113
|
+
.empty{text-align:center;padding:48px;color:#888}
|
|
114
|
+
.toast{position:fixed;bottom:24px;right:24px;background:#1a1a18;color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;opacity:0;transition:opacity 0.3s;z-index:100}
|
|
115
|
+
.toast.show{opacity:1}
|
|
116
|
+
.toast.error{background:#c62828}
|
|
117
|
+
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
|
|
118
|
+
.header-bar a{color:#7c9a6d;text-decoration:none;font-size:14px;font-weight:600}
|
|
119
|
+
.header-bar a:hover{text-decoration:underline}
|
|
120
|
+
.help-text{font-size:12px;color:#888;margin-top:2px}
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
<div class="header-bar">
|
|
125
|
+
<div>
|
|
126
|
+
<h1>Nibgate Admin</h1>
|
|
127
|
+
<p class="sub">Manage gating, pricing, and settings for your content.</p>
|
|
128
|
+
</div>
|
|
129
|
+
<a href="/nibgate.json" target="_blank">View manifest →</a>
|
|
130
|
+
</div>
|
|
131
|
+
<div id="app"><div class="empty">Loading resources...</div></div>
|
|
132
|
+
<div id="toast" class="toast"></div>
|
|
133
|
+
<script>
|
|
134
|
+
const API = '${apiBase}';
|
|
135
|
+
let fields = [];
|
|
136
|
+
let items = [];
|
|
137
|
+
|
|
138
|
+
async function load() {
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch(API + '/resources');
|
|
141
|
+
const data = await res.json();
|
|
142
|
+
if (!data.success) throw new Error(data.error || 'Failed to load');
|
|
143
|
+
fields = data.fields || [];
|
|
144
|
+
items = data.items || [];
|
|
145
|
+
render();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
document.getElementById('app').innerHTML = '<div class="empty">Error loading: ' + err.message + '</div>';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function statusLabel(item) {
|
|
152
|
+
if (item.humanAccess === 'paid') return '<span class="status status-gated">Gated</span>';
|
|
153
|
+
if (item.humanAccess === 'blocked') return '<span class="status status-hidden">Blocked</span>';
|
|
154
|
+
return '<span class="status status-open">Open</span>';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function render() {
|
|
158
|
+
const app = document.getElementById('app');
|
|
159
|
+
if (!items.length) {
|
|
160
|
+
app.innerHTML = '<div class="empty">No resources configured yet. Add resources through the Nibgate SDK and they will appear here after the next manifest sync.</div>';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
app.innerHTML = items.map(item => {
|
|
164
|
+
const humanAccess = fields.find(f => f.name==='humanAccess');
|
|
165
|
+
const agentAccess = fields.find(f => f.name==='agentAccess');
|
|
166
|
+
const unlockMode = fields.find(f => f.name==='unlockMode');
|
|
167
|
+
return '<div class="card" data-id="' + item.id + '">' +
|
|
168
|
+
'<div class="card-header">' +
|
|
169
|
+
'<h3>' + escapeHtml(item.title || item.id) + '</h3>' +
|
|
170
|
+
statusLabel(item) +
|
|
171
|
+
'</div>' +
|
|
172
|
+
'<div class="field-row">' +
|
|
173
|
+
selectField('humanAccess', 'Human access', humanAccess?.options || ['free','paid','blocked'], item.humanAccess) +
|
|
174
|
+
selectField('agentAccess', 'Agent access', agentAccess?.options || ['free','paid','blocked'], item.agentAccess) +
|
|
175
|
+
'</div>' +
|
|
176
|
+
'<div class="field-row">' +
|
|
177
|
+
selectField('unlockMode', 'Unlock mode', unlockMode?.options || ['one_time'], item.unlockMode) +
|
|
178
|
+
selectField('paymentRail', 'Payment rail', ['gateway','transfer'], item.paymentRail || 'gateway') +
|
|
179
|
+
'</div>' +
|
|
180
|
+
'<div class="field-row">' +
|
|
181
|
+
textField('price', 'Price (' + (item.currency || 'USDC') + ')', item.price) +
|
|
182
|
+
textField('currency', 'Currency', item.currency || 'USDC') +
|
|
183
|
+
'</div>' +
|
|
184
|
+
'<div class="field-row">' +
|
|
185
|
+
textField('recipient', 'Recipient wallet', item.recipient || '') +
|
|
186
|
+
checkboxField('ratingsEnabled', 'Enable onchain ratings', item.ratingsEnabled !== false) +
|
|
187
|
+
'</div>' +
|
|
188
|
+
'<div class="field-row">' +
|
|
189
|
+
selectField('type', 'Content type', ['article','music','video','image'], item.type || 'article') +
|
|
190
|
+
checkboxField('publishToNibgate', 'Publish to discovery', item.publishToNibgate !== false) +
|
|
191
|
+
'</div>' +
|
|
192
|
+
'<div><button class="save-btn" onclick="save(this)">Save</button></div>' +
|
|
193
|
+
'</div>';
|
|
194
|
+
}).join('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function textField(name, label, value) {
|
|
198
|
+
return '<div class="field"><label>' + label + '</label><input type="text" name="' + name + '" value="' + escapeAttr(value || '') + '" /></div>';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function selectField(name, label, options, value) {
|
|
202
|
+
const opts = (options || []).map(o => '<option value="' + o + '"' + (o === value ? ' selected' : '') + '>' + o + '</option>').join('');
|
|
203
|
+
return '<div class="field"><label>' + label + '</label><select name="' + name + '">' + opts + '</select></div>';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkboxField(name, label, checked) {
|
|
207
|
+
return '<div class="field"><label>' + label + '</label><div class="checkbox-row"><input type="checkbox" name="' + name + '"' + (checked ? ' checked' : '') + ' /></div></div>';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeHtml(str) {
|
|
211
|
+
const div = document.createElement('div');
|
|
212
|
+
div.textContent = str;
|
|
213
|
+
return div.innerHTML;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function escapeAttr(str) {
|
|
217
|
+
return String(str).replace(/"/g,'"').replace(/'/g,''');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function save(btn) {
|
|
221
|
+
const card = btn.closest('.card');
|
|
222
|
+
const id = card.dataset.id;
|
|
223
|
+
const inputs = card.querySelectorAll('[name]');
|
|
224
|
+
const body = {};
|
|
225
|
+
inputs.forEach(inp => {
|
|
226
|
+
if (inp.type === 'checkbox') body[inp.name] = inp.checked;
|
|
227
|
+
else body[inp.name] = inp.value;
|
|
228
|
+
});
|
|
229
|
+
btn.disabled = true;
|
|
230
|
+
btn.textContent = 'Saving...';
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch(API + '/resources/' + encodeURIComponent(id), {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'content-type': 'application/json' },
|
|
235
|
+
body: JSON.stringify(body)
|
|
236
|
+
});
|
|
237
|
+
const data = await res.json();
|
|
238
|
+
if (!data.success) throw new Error(data.error || 'Save failed');
|
|
239
|
+
toast('Saved ' + (data.item.title || id), 'success');
|
|
240
|
+
load();
|
|
241
|
+
} catch (err) {
|
|
242
|
+
toast(err.message, 'error');
|
|
243
|
+
btn.disabled = false;
|
|
244
|
+
btn.textContent = 'Save';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function toast(msg, type) {
|
|
249
|
+
const el = document.getElementById('toast');
|
|
250
|
+
el.textContent = msg;
|
|
251
|
+
el.className = 'toast ' + (type==='error'?'error':'') + ' show';
|
|
252
|
+
setTimeout(() => { el.classList.remove('show'); }, 3000);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
load();
|
|
256
|
+
</script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export { adminPageHtml };
|
package/src/server/index.js
CHANGED
|
@@ -13,3 +13,5 @@ export { normalizeServerResource as normalizeResource, normalizeAccessPolicy, no
|
|
|
13
13
|
export const server = createNibgateServer();
|
|
14
14
|
export { NIBGATE_CONTENT_SETTING_FIELDS, createNibgateContentSettings, settingsToAccessPolicy, settingsToUnlockPolicy } from '../core/settings.js';
|
|
15
15
|
export { PAYMENT_RAILS, normalizePaymentRail } from '../core/payment.js';
|
|
16
|
+
export { createAdminApi, adminPageHtml } from './admin.js';
|
|
17
|
+
export { createFileStore, createMemoryStore } from './admin-store.js';
|
package/src/server.d.ts
CHANGED
|
@@ -218,3 +218,25 @@ export declare function normalizeResource(resource?: NibgateServerResource | str
|
|
|
218
218
|
export declare function validateResourceMetadata(resource?: NibgateServerResource | string, options?: Record<string, unknown>): NibgateMetadataValidation;
|
|
219
219
|
export declare function normalizeAccessPolicy(access?: NibgateAccessMode | NibgateAccessPolicy): Required<Pick<NibgateAccessPolicy, 'humans' | 'agents'>>;
|
|
220
220
|
export declare function normalizeUnlockPolicy(unlock?: NibgateUnlockMode | NibgateUnlockPolicy): Required<Pick<NibgateUnlockPolicy, 'mode'>> & NibgateUnlockPolicy;
|
|
221
|
+
|
|
222
|
+
// Admin API
|
|
223
|
+
export interface NibgateAdminStore {
|
|
224
|
+
list(): Record<string, unknown>[];
|
|
225
|
+
get(id: string): Record<string, unknown> | null;
|
|
226
|
+
set(id: string, settings: Record<string, unknown>): Record<string, unknown>;
|
|
227
|
+
remove(id: string): boolean;
|
|
228
|
+
}
|
|
229
|
+
export interface NibgateAdminApi {
|
|
230
|
+
handleList(req: Request, res: Record<string, unknown>): Promise<Response>;
|
|
231
|
+
handleGet(req: Request, res: Record<string, unknown>): Promise<Response>;
|
|
232
|
+
handleUpdate(req: Request, res: Record<string, unknown>): Promise<Response>;
|
|
233
|
+
handleDelete(req: Request, res: Record<string, unknown>): Promise<Response>;
|
|
234
|
+
buildResourceFromSettings(id: string, settings: Record<string, unknown>): NibgateServerResource;
|
|
235
|
+
router(expressModule: Record<string, unknown>): unknown;
|
|
236
|
+
store: NibgateAdminStore;
|
|
237
|
+
settingsFields: Record<string, unknown>[];
|
|
238
|
+
}
|
|
239
|
+
export declare function createAdminApi(options: { store: NibgateAdminStore; title?: string; authorize?: (req: Request) => boolean }): NibgateAdminApi;
|
|
240
|
+
export declare function createFileStore(options?: { path?: string }): NibgateAdminStore;
|
|
241
|
+
export declare function createMemoryStore(): NibgateAdminStore;
|
|
242
|
+
export declare function adminPageHtml(options?: { title?: string; apiBase?: string }): string;
|