@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 CHANGED
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: nibgate-sdk
3
- description: Use when integrating, auditing, documenting, or modifying creator-owned paid content flows with the @nibgate/sdk package. Covers installing the package, adding the Nibgate widget, defining resources, protecting server routes, emitting content/unlock events, creating nibgate.json discovery metadata, wiring Circle Gateway/x402 one-time unlocks, and avoiding fake payment or client-only gating patterns.
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 actually serves the content. Nibgate Hub verifies the site, indexes public metadata, and records events; the creator site keeps the protected payload and payment receiver logic.
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
- The guide is intentionally compact so agents can follow the correct package, widget, server gating, payment, and event patterns without guessing. If an agent is modifying a creator site, tell it to read `SKILL.md` before editing routes or checkout code.
23
+ Read this entire file before editing any routes or checkout code. The guide follows the full integration order.
24
24
 
25
- Use browser helpers from `@nibgate/sdk` and access enforcement from `@nibgate/sdk/server`:
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 and receives browser-side content, view, unlock, receipt, and reputation events. If the widget loads after app code, package events queue and flush when ready.
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
- ## Resource Shape
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 public content types are `article`, `image`, `music`, and `video`. Use stable external ids; changing ids breaks continuity for Explore, receipts, and reputation.
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
- ## Browser Page Wiring
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 UI:
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 custom flows, use `checkResourceAccess(resource, options)` directly. The browser can start checkout and report state, but the server must verify payment and issue unlock proof before returning protected content.
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
- ## Server Access Route
168
+ For custom flows, use `checkResourceAccess(resource, options)` directly.
108
169
 
109
- Protect paid content in a server route, API handler, middleware, guard, or signed file endpoint:
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 { createCircleGatewayServer } from '@nibgate/sdk/server'
177
+ import { nibgateServer } from './nibgate-resource'
113
178
 
114
- const nibgate = createCircleGatewayServer({
115
- origin: process.env.NIBGATE_SITE_ORIGIN,
116
- secret: process.env.NIBGATE_SECRET,
117
- network: process.env.NIBGATE_PAYMENT_NETWORK || 'eip155:5042002'
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
- export function GET(request: Request) {
121
- return nibgate.accessResponse(request, resource, {
122
- getContent: async () => new Response(protectedHtml, {
123
- headers: { 'content-type': 'text/html; charset=utf-8' }
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
- The route should return a real `402 Payment Required` challenge until the request has a valid payment proof or signed unlock proof.
252
+ ### Admin page
130
253
 
131
- ## Hardcoded Or File-Based Content
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
- Hardcoded content, MDX files, Markdown files, static JSON files, and repo-local media metadata can be gated when the protected payload stays server-side until access is allowed.
265
+ ---
134
266
 
135
- Safe pattern:
267
+ ## 8. Payments (Circle Gateway / x402 on Arc Testnet)
136
268
 
137
- 1. keep the public teaser, title, price, and metadata in the page;
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
- Unsafe pattern:
271
+ ### How payment works
142
272
 
143
- - rendering the paid body into public HTML and hiding it with CSS;
144
- - bundling the paid body into client JavaScript;
145
- - putting private text into `nibgate.json`, JSON-LD, meta tags, page source, or static exports;
146
- - exposing an open API route that returns the protected body without checking payment proof.
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
- ## Required Env Vars
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
- Creator site:
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, such as site id, public token, API base, or reputation contract address. Never ship `NIBGATE_SECRET`, private keys, R2 credentials, Resend keys, database URLs, or privileged payment credentials to browser code.
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
- ## Payments
400
+ ---
166
401
 
167
- For v1, treat `unlock.mode: 'one_time'` with Circle Gateway/x402 as the production path. A Gateway/x402 payment signature is a payment proof. A normal wallet message signature is not payment.
402
+ ## 12. Common Gotchas
168
403
 
169
- Use `NIBGATE_BUYER_PRIVATE_KEY` only in local demos or controlled tests. In production, the visitor or agent wallet signs/pays the returned `PAYMENT-REQUIRED` challenge.
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
- ## Reputation
413
+ ---
172
414
 
173
- After a verified unlock, rating UI can submit onchain ratings for the same resource. Reputation-critical inputs should be tied to unlock/payment proof and indexed from chain activity. Page views, scroll depth, and referrers are analytics signals, not trust by themselves.
415
+ ## 13. Never Do These
174
416
 
175
- ## Never Do These
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
- - Do not hide paid HTML with CSS or client state after rendering it publicly.
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**: use route handlers for `/nibgate.json` and `/api/nibgate/access`; protect content before rendering paid payloads.
187
- - **Express/NestJS**: use middleware, controllers, or guards around protected routes.
188
- - **Astro/SvelteKit/Remix**: use SSR/server routes or endpoints; plain static builds need a protected API or signed URL for private payloads.
189
- - **CMS apps**: save Nibgate resource settings beside each content record, including type, price, currency, receiver wallet, access policy, unlock mode, and discovery preference.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nibgate/sdk",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Framework-agnostic browser and server package for creator-owned gated content, unlock events, and receipts.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -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
  },
@@ -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
- win.localStorage.setItem(proofStorageKey(resource), String(proof));
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>;
@@ -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') || 'circle-gateway') === 'circle-gateway') {
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,'&quot;').replace(/'/g,'&#39;');
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 };
@@ -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;